286 lines
7.7 KiB
TypeScript
286 lines
7.7 KiB
TypeScript
|
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
|
||
|
);
|