vega-frontend-monorepo/libs/positions/src/lib/positions-metrics-data-provider.ts

286 lines
7.7 KiB
TypeScript
Raw Normal View History

import { gql } from '@apollo/client';
import produce from 'immer';
import BigNumber from 'bignumber.js';
import sortBy from 'lodash/sortBy';
import type {
PositionsMetrics,
PositionsMetrics_party,
} from './__generated__/PositionsMetrics';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type {
PositionsMetricsSubscription,
PositionsMetricsSubscription_positions,
} from './__generated__/PositionsMetricsSubscription';
import { AccountType } from '@vegaprotocol/types';
import type { MarketTradingMode } from '@vegaprotocol/types';
export interface Position {
marketName: string;
averageEntryPrice: string;
capitalUtilisation: number;
currentLeverage: number;
assetDecimals: number;
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 {
party: PositionsMetrics_party | null;
positions: Position[] | null;
}
const POSITIONS_METRICS_FRAGMENT = gql`
fragment PositionMetricsFields on Position {
realisedPNL
openVolume
unrealisedPNL
averageEntryPrice
updatedAt
market {
id
name
decimalPlaces
positionDecimalPlaces
tradingMode
tradableInstrument {
instrument {
name
}
}
data {
markPrice
}
}
}
`;
const POSITION_METRICS_QUERY = gql`
${POSITIONS_METRICS_FRAGMENT}
query PositionsMetrics($partyId: ID!) {
party(id: $partyId) {
id
accounts {
type
asset {
id
decimals
}
balance
market {
id
}
}
marginsConnection {
edges {
node {
market {
id
}
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
asset {
symbol
}
}
}
}
positionsConnection {
edges {
node {
...PositionMetricsFields
}
}
}
}
}
`;
export const POSITIONS_METRICS_SUBSCRIPTION = gql`
${POSITIONS_METRICS_FRAGMENT}
subscription PositionsMetricsSubscription($partyId: ID!) {
positions(partyId: $partyId) {
...PositionMetricsFields
}
}
`;
export const getMetrics = (data: PositionsMetrics_party | null): Position[] => {
if (!data || !data.positionsConnection.edges) {
return [];
}
const metrics: Position[] = [];
data.positionsConnection.edges.forEach((position) => {
const market = position.node.market;
const marketData = market.data;
const marginLevel = data.marginsConnection.edges?.find(
(margin) => margin.node.market.id === market.id
)?.node;
const marginAccount = data.accounts?.find(
(account) => account.market?.id === market.id
);
if (!marginAccount || !marginLevel || !marketData) {
return;
}
const generalAccount = data.accounts?.find(
(account) =>
account.asset.id === marginAccount.asset.id &&
account.type === AccountType.General
);
const assetDecimals = marginAccount.asset.decimals;
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market;
const openVolume = new BigNumber(position.node.openVolume).dividedBy(
10 ** positionDecimalPlaces
);
const marginAccountBalance = marginAccount
? new BigNumber(marginAccount.balance).dividedBy(10 ** assetDecimals)
: new BigNumber(0);
const generalAccountBalance = generalAccount
? new BigNumber(generalAccount.balance).dividedBy(10 ** assetDecimals)
: new BigNumber(0);
const markPrice = new BigNumber(marketData.markPrice).dividedBy(
10 ** marketDecimalPlaces
);
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);
const marginMaintenance = new BigNumber(
marginLevel.maintenanceLevel
).multipliedBy(marketDecimalPlaces);
const marginSearch = new BigNumber(marginLevel.searchLevel).multipliedBy(
marketDecimalPlaces
);
const marginInitial = new BigNumber(marginLevel.initialLevel).multipliedBy(
marketDecimalPlaces
);
const searchPrice = openVolume.isEqualTo(0)
? markPrice
: marginSearch
.minus(marginAccountBalance)
.dividedBy(openVolume)
.plus(markPrice);
const liquidationPrice = openVolume.isEqualTo(0)
? markPrice
: marginMaintenance
.minus(marginAccountBalance)
.minus(generalAccountBalance)
.dividedBy(openVolume)
.plus(markPrice);
const lowMarginLevel =
marginAccountBalance.isLessThan(
marginSearch.plus(marginInitial.minus(marginSearch).dividedBy(2))
) && generalAccountBalance.isLessThan(marginInitial.minus(marginSearch));
metrics.push({
marketName: market.name,
averageEntryPrice: position.node.averageEntryPrice,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage.toNumber(),
marketDecimalPlaces,
positionDecimalPlaces,
assetDecimals,
assetSymbol: marginLevel.asset.symbol,
totalBalance: totalBalance.multipliedBy(10 ** assetDecimals).toFixed(),
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 = (
data: Data,
delta: PositionsMetricsSubscription_positions
) => {
if (!data.party?.positionsConnection.edges) {
return data;
}
const edges = produce(data.party.positionsConnection.edges, (draft) => {
const index = draft.findIndex(
(edge) => edge.node.market.id === delta.market.id
);
if (index !== -1) {
draft[index].node = delta;
} else {
draft.push({ __typename: 'PositionEdge', node: delta });
}
});
const party = produce(data.party, (draft) => {
draft.positionsConnection.edges = edges;
});
if (party === data.party) {
return data;
}
return {
party,
positions: getMetrics(party),
};
};
const getData = (responseData: PositionsMetrics): Data => {
return {
party: responseData.party,
positions: sortBy(getMetrics(responseData.party), 'updatedAt').reverse(),
};
};
const getDelta = (
subscriptionData: PositionsMetricsSubscription
): PositionsMetricsSubscription_positions => subscriptionData.positions;
export const positionsMetricsDataProvider = makeDataProvider<
PositionsMetrics,
Data,
PositionsMetricsSubscription,
PositionsMetricsSubscription_positions
>(
POSITION_METRICS_QUERY,
POSITIONS_METRICS_SUBSCRIPTION,
update,
getData,
getDelta
);