Feature/473 position margin levels (#902)
* feat(#473): add positions metrics data provider * feat(#473) add positions stats * feat(#473) add positions stats * feat(#473): add positions stats * feat(#473): add positions stats * feat(#473): position metrics, test and refactoring * feat(#473): add unit tests to positions table * feat(#473): fix spelling, order positions by updated at desc * feat(#473): protect from division by 0 * feat(#473): fix trading positions e2e tests * feat(#473): fix e2e data mocks * feat(#473): post code review clean up
This commit is contained in:
parent
aaedbdde8c
commit
08b7c9769a
@ -18,7 +18,7 @@ describe('positions', () => {
|
||||
|
||||
cy.getByTestId('tab-positions').should('be.visible');
|
||||
cy.getByTestId('tab-positions')
|
||||
.get('[col-id="market.tradableInstrument.instrument.code"]')
|
||||
.get('[col-id="marketName"]')
|
||||
.should('be.visible')
|
||||
.each(($marketSymbol) => {
|
||||
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
|
||||
|
163
apps/trading-e2e/src/support/mocks/generate-positions-metrics.ts
Normal file
163
apps/trading-e2e/src/support/mocks/generate-positions-metrics.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type {
|
||||
PositionsMetrics,
|
||||
PositionsMetrics_party_positionsConnection_edges_node,
|
||||
} from '@vegaprotocol/positions';
|
||||
import { MarketTradingMode, AccountType } from '@vegaprotocol/types';
|
||||
|
||||
export const generatePositionsMetrics = (
|
||||
override?: PartialDeep<PositionsMetrics>
|
||||
): PositionsMetrics => {
|
||||
const nodes: PositionsMetrics_party_positionsConnection_edges_node[] = [
|
||||
{
|
||||
realisedPNL: '0',
|
||||
openVolume: '6',
|
||||
unrealisedPNL: '895000',
|
||||
averageEntryPrice: '1129935',
|
||||
updatedAt: '2022-07-28T15:09:34.441143Z',
|
||||
market: {
|
||||
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
tradingMode: MarketTradingMode.Continuous,
|
||||
data: {
|
||||
markPrice: '17588787',
|
||||
__typename: 'MarketData',
|
||||
},
|
||||
decimalPlaces: 5,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
__typename: 'Instrument',
|
||||
},
|
||||
__typename: 'TradableInstrument',
|
||||
},
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'Position',
|
||||
},
|
||||
{
|
||||
realisedPNL: '0',
|
||||
openVolume: '1',
|
||||
unrealisedPNL: '-22519',
|
||||
averageEntryPrice: '84400088',
|
||||
updatedAt: '2022-07-28T14:53:54.725477Z',
|
||||
market: {
|
||||
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
|
||||
name: 'Tesla Quarterly (30 Jun 2022)',
|
||||
tradingMode: MarketTradingMode.Continuous,
|
||||
data: {
|
||||
markPrice: '84377569',
|
||||
__typename: 'MarketData',
|
||||
},
|
||||
decimalPlaces: 5,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Tesla Quarterly (30 Jun 2022)',
|
||||
__typename: 'Instrument',
|
||||
},
|
||||
__typename: 'TradableInstrument',
|
||||
},
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'Position',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultResult: PositionsMetrics = {
|
||||
party: {
|
||||
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||
accounts: [
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.General,
|
||||
balance: '100000000',
|
||||
market: null,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id-2',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.Margin,
|
||||
balance: '1000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.Margin,
|
||||
balance: '1000',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id-2',
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
marginsConnection: {
|
||||
__typename: 'MarginConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
symbol: 'tDAI',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376 ',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
symbol: 'tEURO',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
positionsConnection: {
|
||||
__typename: 'PositionConnection',
|
||||
edges: nodes.map((node) => ({ __typename: 'PositionEdge', node })),
|
||||
},
|
||||
__typename: 'Party',
|
||||
},
|
||||
};
|
||||
|
||||
return merge(defaultResult, override);
|
||||
};
|
@ -9,6 +9,7 @@ import { generateMarket } from './mocks/generate-market';
|
||||
import { generateMarketInfoQuery } from './mocks/generate-market-info-query';
|
||||
import { generateOrders } from './mocks/generate-orders';
|
||||
import { generatePositions } from './mocks/generate-positions';
|
||||
import { generatePositionsMetrics } from './mocks/generate-positions-metrics';
|
||||
import { generateTrades } from './mocks/generate-trades';
|
||||
|
||||
export const mockTradingPage = (
|
||||
@ -28,6 +29,7 @@ export const mockTradingPage = (
|
||||
aliasQuery(req, 'Orders', generateOrders());
|
||||
aliasQuery(req, 'Accounts', generateAccounts());
|
||||
aliasQuery(req, 'Positions', generatePositions());
|
||||
aliasQuery(req, 'PositionsMetrics', generatePositionsMetrics());
|
||||
aliasQuery(
|
||||
req,
|
||||
'DealTicketQuery',
|
||||
|
28
libs/positions/.storybook/main.js
Normal file
28
libs/positions/.storybook/main.js
Normal file
@ -0,0 +1,28 @@
|
||||
const rootMain = require('../../../.storybook/main');
|
||||
|
||||
module.exports = {
|
||||
...rootMain,
|
||||
|
||||
core: { ...rootMain.core, builder: 'webpack5' },
|
||||
|
||||
stories: [
|
||||
...rootMain.stories,
|
||||
'../src/lib/**/*.stories.mdx',
|
||||
'../src/lib/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
...rootMain.addons,
|
||||
'@nrwl/react/plugins/storybook',
|
||||
'storybook-addon-themes',
|
||||
],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
// apply any global webpack configs that might have been specified in .storybook/main.js
|
||||
if (rootMain.webpackFinal) {
|
||||
config = await rootMain.webpackFinal(config, { configType });
|
||||
}
|
||||
|
||||
// add your own webpack tweaks if needed
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
1
libs/positions/.storybook/preview-head.html
Normal file
1
libs/positions/.storybook/preview-head.html
Normal file
@ -0,0 +1 @@
|
||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
50
libs/positions/.storybook/preview.js
Normal file
50
libs/positions/.storybook/preview.js
Normal file
@ -0,0 +1,50 @@
|
||||
import './styles.scss';
|
||||
import { ThemeContext } from '@vegaprotocol/react-helpers';
|
||||
import { useEffect, useState } from 'react';
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
backgrounds: { disable: true },
|
||||
themes: {
|
||||
default: 'dark',
|
||||
list: [
|
||||
{ name: 'dark', class: ['dark', 'bg-black'], color: '#000' },
|
||||
{ name: 'light', class: '', color: '#FFF' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story, context) => {
|
||||
// storybook-addon-themes doesnt seem to provide the current selected
|
||||
// theme in context, we need to provid it in JS as some components
|
||||
// rely on it for rendering
|
||||
const [theme, setTheme] = useState(context.parameters.themes.default);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
if (mutationList.length) {
|
||||
const body = mutationList[0].target;
|
||||
if (body.classList.contains('dark')) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { attributes: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: 500 }}>
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<Story />
|
||||
</ThemeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
];
|
3
libs/positions/.storybook/styles.scss
Normal file
3
libs/positions/.storybook/styles.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
19
libs/positions/.storybook/tsconfig.json
Normal file
19
libs/positions/.storybook/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": ""
|
||||
},
|
||||
"files": [
|
||||
"../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../**/*.spec.ts",
|
||||
"../**/*.spec.js",
|
||||
"../**/*.spec.tsx",
|
||||
"../**/*.spec.jsx"
|
||||
],
|
||||
"include": ["../src/**/*", "*.js"]
|
||||
}
|
10
libs/positions/postcss.config.js
Normal file
10
libs/positions/postcss.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -38,6 +38,37 @@
|
||||
"jestConfig": "libs/positions/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"executor": "@nrwl/storybook:storybook",
|
||||
"options": {
|
||||
"uiFramework": "@storybook/react",
|
||||
"port": 4400,
|
||||
"config": {
|
||||
"configFolder": "libs/positions/.storybook"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"executor": "@nrwl/storybook:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"uiFramework": "@storybook/react",
|
||||
"outputPath": "dist/storybook/positions",
|
||||
"config": {
|
||||
"configFolder": "libs/positions/.storybook"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/positions-container';
|
||||
export { positionsMetricsDataProvider } from './lib/positions-metrics-data-provider';
|
||||
export * from './lib/positions-data-provider';
|
||||
export * from './lib/__generated__/PositionsMetrics';
|
||||
export * from './lib/__generated__/Positions';
|
||||
export * from './lib/__generated__/PositionDetails';
|
||||
|
@ -33,68 +33,16 @@ export interface PositionDetails_market_data {
|
||||
market: PositionDetails_market_data_market;
|
||||
}
|
||||
|
||||
export interface PositionDetails_market_tradableInstrument_instrument_metadata {
|
||||
__typename: "InstrumentMetadata";
|
||||
/**
|
||||
* An arbitrary list of tags to associated to associate to the Instrument (string list)
|
||||
*/
|
||||
tags: string[] | null;
|
||||
}
|
||||
|
||||
export interface PositionDetails_market_tradableInstrument_instrument_product_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface PositionDetails_market_tradableInstrument_instrument_product {
|
||||
__typename: "Future";
|
||||
/**
|
||||
* The name of the asset (string)
|
||||
*/
|
||||
settlementAsset: PositionDetails_market_tradableInstrument_instrument_product_settlementAsset;
|
||||
/**
|
||||
* String representing the quote (e.g. BTCUSD -> USD is quote)
|
||||
*/
|
||||
quoteName: string;
|
||||
}
|
||||
|
||||
export interface PositionDetails_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Uniquely identify an instrument across all instruments available on Vega (string)
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Metadata for this instrument
|
||||
*/
|
||||
metadata: PositionDetails_market_tradableInstrument_instrument_metadata;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
||||
*/
|
||||
product: PositionDetails_market_tradableInstrument_instrument_product;
|
||||
}
|
||||
|
||||
export interface PositionDetails_market_tradableInstrument {
|
||||
|
109
libs/positions/src/lib/__generated__/PositionMetricsFields.ts
generated
Normal file
109
libs/positions/src/lib/__generated__/PositionMetricsFields.ts
generated
Normal file
@ -0,0 +1,109 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketTradingMode } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: PositionMetricsFields
|
||||
// ====================================================
|
||||
|
||||
export interface PositionMetricsFields_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PositionMetricsFields_market_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of or reference to a fully specified instrument.
|
||||
*/
|
||||
instrument: PositionMetricsFields_market_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface PositionMetricsFields_market_data {
|
||||
__typename: "MarketData";
|
||||
/**
|
||||
* the mark price (actually an unsigned int)
|
||||
*/
|
||||
markPrice: string;
|
||||
}
|
||||
|
||||
export interface PositionMetricsFields_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Market full name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||
* number denominated in the currency of the Market. (uint64)
|
||||
*
|
||||
* Examples:
|
||||
* Currency Balance decimalPlaces Real Balance
|
||||
* GBP 100 0 GBP 100
|
||||
* GBP 100 2 GBP 1.00
|
||||
* GBP 100 4 GBP 0.01
|
||||
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||
*
|
||||
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||
*/
|
||||
decimalPlaces: number;
|
||||
/**
|
||||
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
|
||||
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
|
||||
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
|
||||
*/
|
||||
positionDecimalPlaces: number;
|
||||
/**
|
||||
* Current mode of execution of the market
|
||||
*/
|
||||
tradingMode: MarketTradingMode;
|
||||
/**
|
||||
* An instance of or reference to a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: PositionMetricsFields_market_tradableInstrument;
|
||||
/**
|
||||
* marketData for the given market
|
||||
*/
|
||||
data: PositionMetricsFields_market_data | null;
|
||||
}
|
||||
|
||||
export interface PositionMetricsFields {
|
||||
__typename: "Position";
|
||||
/**
|
||||
* Realised Profit and Loss (int64)
|
||||
*/
|
||||
realisedPNL: string;
|
||||
/**
|
||||
* Open volume (uint64)
|
||||
*/
|
||||
openVolume: string;
|
||||
/**
|
||||
* Unrealised Profit and Loss (int64)
|
||||
*/
|
||||
unrealisedPNL: string;
|
||||
/**
|
||||
* Average entry price for this position
|
||||
*/
|
||||
averageEntryPrice: string;
|
||||
/**
|
||||
* RFC3339Nano time the position was updated
|
||||
*/
|
||||
updatedAt: string | null;
|
||||
/**
|
||||
* Market relating to this position
|
||||
*/
|
||||
market: PositionMetricsFields_market;
|
||||
}
|
@ -33,68 +33,16 @@ export interface PositionSubscribe_positions_market_data {
|
||||
market: PositionSubscribe_positions_market_data_market;
|
||||
}
|
||||
|
||||
export interface PositionSubscribe_positions_market_tradableInstrument_instrument_metadata {
|
||||
__typename: "InstrumentMetadata";
|
||||
/**
|
||||
* An arbitrary list of tags to associated to associate to the Instrument (string list)
|
||||
*/
|
||||
tags: string[] | null;
|
||||
}
|
||||
|
||||
export interface PositionSubscribe_positions_market_tradableInstrument_instrument_product_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface PositionSubscribe_positions_market_tradableInstrument_instrument_product {
|
||||
__typename: "Future";
|
||||
/**
|
||||
* The name of the asset (string)
|
||||
*/
|
||||
settlementAsset: PositionSubscribe_positions_market_tradableInstrument_instrument_product_settlementAsset;
|
||||
/**
|
||||
* String representing the quote (e.g. BTCUSD -> USD is quote)
|
||||
*/
|
||||
quoteName: string;
|
||||
}
|
||||
|
||||
export interface PositionSubscribe_positions_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Uniquely identify an instrument across all instruments available on Vega (string)
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Metadata for this instrument
|
||||
*/
|
||||
metadata: PositionSubscribe_positions_market_tradableInstrument_instrument_metadata;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
||||
*/
|
||||
product: PositionSubscribe_positions_market_tradableInstrument_instrument_product;
|
||||
}
|
||||
|
||||
export interface PositionSubscribe_positions_market_tradableInstrument {
|
||||
|
52
libs/positions/src/lib/__generated__/Positions.ts
generated
52
libs/positions/src/lib/__generated__/Positions.ts
generated
@ -33,68 +33,16 @@ export interface Positions_party_positions_market_data {
|
||||
market: Positions_party_positions_market_data_market;
|
||||
}
|
||||
|
||||
export interface Positions_party_positions_market_tradableInstrument_instrument_metadata {
|
||||
__typename: "InstrumentMetadata";
|
||||
/**
|
||||
* An arbitrary list of tags to associated to associate to the Instrument (string list)
|
||||
*/
|
||||
tags: string[] | null;
|
||||
}
|
||||
|
||||
export interface Positions_party_positions_market_tradableInstrument_instrument_product_settlementAsset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
/**
|
||||
* The full name of the asset (e.g: Great British Pound)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface Positions_party_positions_market_tradableInstrument_instrument_product {
|
||||
__typename: "Future";
|
||||
/**
|
||||
* The name of the asset (string)
|
||||
*/
|
||||
settlementAsset: Positions_party_positions_market_tradableInstrument_instrument_product_settlementAsset;
|
||||
/**
|
||||
* String representing the quote (e.g. BTCUSD -> USD is quote)
|
||||
*/
|
||||
quoteName: string;
|
||||
}
|
||||
|
||||
export interface Positions_party_positions_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Uniquely identify an instrument across all instruments available on Vega (string)
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Metadata for this instrument
|
||||
*/
|
||||
metadata: Positions_party_positions_market_tradableInstrument_instrument_metadata;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
||||
*/
|
||||
product: Positions_party_positions_market_tradableInstrument_instrument_product;
|
||||
}
|
||||
|
||||
export interface Positions_party_positions_market_tradableInstrument {
|
||||
|
251
libs/positions/src/lib/__generated__/PositionsMetrics.ts
generated
Normal file
251
libs/positions/src/lib/__generated__/PositionsMetrics.ts
generated
Normal file
@ -0,0 +1,251 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AccountType, MarketTradingMode } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: PositionsMetrics
|
||||
// ====================================================
|
||||
|
||||
export interface PositionsMetrics_party_accounts_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The id of the asset
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The precision of the asset
|
||||
*/
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_accounts_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_accounts {
|
||||
__typename: "Account";
|
||||
/**
|
||||
* Account type (General, Margin, etc)
|
||||
*/
|
||||
type: AccountType;
|
||||
/**
|
||||
* Asset, the 'currency'
|
||||
*/
|
||||
asset: PositionsMetrics_party_accounts_asset;
|
||||
/**
|
||||
* Balance as string - current account balance (approx. as balances can be updated several times per second)
|
||||
*/
|
||||
balance: string;
|
||||
/**
|
||||
* Market (only relevant to margin accounts)
|
||||
*/
|
||||
market: PositionsMetrics_party_accounts_market | null;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_marginsConnection_edges_node_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_marginsConnection_edges_node_asset {
|
||||
__typename: "Asset";
|
||||
/**
|
||||
* The symbol of the asset (e.g: GBP)
|
||||
*/
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_marginsConnection_edges_node {
|
||||
__typename: "MarginLevels";
|
||||
/**
|
||||
* market in which the margin is required for this party
|
||||
*/
|
||||
market: PositionsMetrics_party_marginsConnection_edges_node_market;
|
||||
/**
|
||||
* minimal margin for the position to be maintained in the network (unsigned int actually)
|
||||
*/
|
||||
maintenanceLevel: string;
|
||||
/**
|
||||
* if the margin is between maintenance and search, the network will initiate a collateral search (unsigned int actually)
|
||||
*/
|
||||
searchLevel: string;
|
||||
/**
|
||||
* this is the minimal margin required for a party to place a new order on the network (unsigned int actually)
|
||||
*/
|
||||
initialLevel: string;
|
||||
/**
|
||||
* If the margin of the party is greater than this level, then collateral will be released from the margin account into
|
||||
* the general account of the party for the given asset.
|
||||
*/
|
||||
collateralReleaseLevel: string;
|
||||
/**
|
||||
* asset for the current margins
|
||||
*/
|
||||
asset: PositionsMetrics_party_marginsConnection_edges_node_asset;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_marginsConnection_edges {
|
||||
__typename: "MarginEdge";
|
||||
node: PositionsMetrics_party_marginsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_marginsConnection {
|
||||
__typename: "MarginConnection";
|
||||
/**
|
||||
* The margin levels in this connection
|
||||
*/
|
||||
edges: PositionsMetrics_party_marginsConnection_edges[] | null;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of or reference to a fully specified instrument.
|
||||
*/
|
||||
instrument: PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection_edges_node_market_data {
|
||||
__typename: "MarketData";
|
||||
/**
|
||||
* the mark price (actually an unsigned int)
|
||||
*/
|
||||
markPrice: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection_edges_node_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Market full name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||
* number denominated in the currency of the Market. (uint64)
|
||||
*
|
||||
* Examples:
|
||||
* Currency Balance decimalPlaces Real Balance
|
||||
* GBP 100 0 GBP 100
|
||||
* GBP 100 2 GBP 1.00
|
||||
* GBP 100 4 GBP 0.01
|
||||
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||
*
|
||||
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||
*/
|
||||
decimalPlaces: number;
|
||||
/**
|
||||
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
|
||||
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
|
||||
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
|
||||
*/
|
||||
positionDecimalPlaces: number;
|
||||
/**
|
||||
* Current mode of execution of the market
|
||||
*/
|
||||
tradingMode: MarketTradingMode;
|
||||
/**
|
||||
* An instance of or reference to a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: PositionsMetrics_party_positionsConnection_edges_node_market_tradableInstrument;
|
||||
/**
|
||||
* marketData for the given market
|
||||
*/
|
||||
data: PositionsMetrics_party_positionsConnection_edges_node_market_data | null;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection_edges_node {
|
||||
__typename: "Position";
|
||||
/**
|
||||
* Realised Profit and Loss (int64)
|
||||
*/
|
||||
realisedPNL: string;
|
||||
/**
|
||||
* Open volume (uint64)
|
||||
*/
|
||||
openVolume: string;
|
||||
/**
|
||||
* Unrealised Profit and Loss (int64)
|
||||
*/
|
||||
unrealisedPNL: string;
|
||||
/**
|
||||
* Average entry price for this position
|
||||
*/
|
||||
averageEntryPrice: string;
|
||||
/**
|
||||
* RFC3339Nano time the position was updated
|
||||
*/
|
||||
updatedAt: string | null;
|
||||
/**
|
||||
* Market relating to this position
|
||||
*/
|
||||
market: PositionsMetrics_party_positionsConnection_edges_node_market;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection_edges {
|
||||
__typename: "PositionEdge";
|
||||
node: PositionsMetrics_party_positionsConnection_edges_node;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party_positionsConnection {
|
||||
__typename: "PositionConnection";
|
||||
/**
|
||||
* The positions in this connection
|
||||
*/
|
||||
edges: PositionsMetrics_party_positionsConnection_edges[] | null;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics_party {
|
||||
__typename: "Party";
|
||||
/**
|
||||
* Party identifier
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Collateral accounts relating to a party
|
||||
*/
|
||||
accounts: PositionsMetrics_party_accounts[] | null;
|
||||
/**
|
||||
* Margin level for a market
|
||||
*/
|
||||
marginsConnection: PositionsMetrics_party_marginsConnection;
|
||||
/**
|
||||
* Trading positions relating to a party
|
||||
*/
|
||||
positionsConnection: PositionsMetrics_party_positionsConnection;
|
||||
}
|
||||
|
||||
export interface PositionsMetrics {
|
||||
/**
|
||||
* An entity that is trading on the VEGA network
|
||||
*/
|
||||
party: PositionsMetrics_party | null;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsVariables {
|
||||
partyId: string;
|
||||
}
|
120
libs/positions/src/lib/__generated__/PositionsMetricsSubscription.ts
generated
Normal file
120
libs/positions/src/lib/__generated__/PositionsMetricsSubscription.ts
generated
Normal file
@ -0,0 +1,120 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketTradingMode } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: PositionsMetricsSubscription
|
||||
// ====================================================
|
||||
|
||||
export interface PositionsMetricsSubscription_positions_market_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsSubscription_positions_market_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of or reference to a fully specified instrument.
|
||||
*/
|
||||
instrument: PositionsMetricsSubscription_positions_market_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsSubscription_positions_market_data {
|
||||
__typename: "MarketData";
|
||||
/**
|
||||
* the mark price (actually an unsigned int)
|
||||
*/
|
||||
markPrice: string;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsSubscription_positions_market {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Market full name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||
* number denominated in the currency of the Market. (uint64)
|
||||
*
|
||||
* Examples:
|
||||
* Currency Balance decimalPlaces Real Balance
|
||||
* GBP 100 0 GBP 100
|
||||
* GBP 100 2 GBP 1.00
|
||||
* GBP 100 4 GBP 0.01
|
||||
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||
*
|
||||
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||
*/
|
||||
decimalPlaces: number;
|
||||
/**
|
||||
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
|
||||
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
|
||||
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
|
||||
*/
|
||||
positionDecimalPlaces: number;
|
||||
/**
|
||||
* Current mode of execution of the market
|
||||
*/
|
||||
tradingMode: MarketTradingMode;
|
||||
/**
|
||||
* An instance of or reference to a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: PositionsMetricsSubscription_positions_market_tradableInstrument;
|
||||
/**
|
||||
* marketData for the given market
|
||||
*/
|
||||
data: PositionsMetricsSubscription_positions_market_data | null;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsSubscription_positions {
|
||||
__typename: "Position";
|
||||
/**
|
||||
* Realised Profit and Loss (int64)
|
||||
*/
|
||||
realisedPNL: string;
|
||||
/**
|
||||
* Open volume (uint64)
|
||||
*/
|
||||
openVolume: string;
|
||||
/**
|
||||
* Unrealised Profit and Loss (int64)
|
||||
*/
|
||||
unrealisedPNL: string;
|
||||
/**
|
||||
* Average entry price for this position
|
||||
*/
|
||||
averageEntryPrice: string;
|
||||
/**
|
||||
* RFC3339Nano time the position was updated
|
||||
*/
|
||||
updatedAt: string | null;
|
||||
/**
|
||||
* Market relating to this position
|
||||
*/
|
||||
market: PositionsMetricsSubscription_positions_market;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsSubscription {
|
||||
/**
|
||||
* Subscribe to the positions updates
|
||||
*/
|
||||
positions: PositionsMetricsSubscription_positions;
|
||||
}
|
||||
|
||||
export interface PositionsMetricsSubscriptionVariables {
|
||||
partyId: string;
|
||||
}
|
@ -31,23 +31,8 @@ const POSITIONS_FRAGMENT = gql`
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
id
|
||||
name
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { useRef, useCallback, useMemo } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import merge from 'lodash/merge';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { PositionSubscribe_positions } from './__generated__/PositionSubscribe';
|
||||
import type { Positions_party_positions } from './__generated__/Positions';
|
||||
|
||||
import type { PositionsMetricsSubscription_positions } from './__generated__/PositionsMetricsSubscription';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import PositionsTable, { getRowId } from './positions-table';
|
||||
import { positionsDataProvider } from './positions-data-provider';
|
||||
import PositionsTable from './positions-table';
|
||||
import type { GetRowsParams } from './positions-table';
|
||||
import { positionsMetricsDataProvider as dataProvider } from './positions-metrics-data-provider';
|
||||
import type { Data, Position } from './positions-metrics-data-provider';
|
||||
|
||||
interface PositionsManagerProps {
|
||||
partyId: string;
|
||||
@ -17,45 +15,39 @@ interface PositionsManagerProps {
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const update = useCallback(
|
||||
({ delta }: { delta: PositionSubscribe_positions }) => {
|
||||
const update: Positions_party_positions[] = [];
|
||||
const add: Positions_party_positions[] = [];
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
const rowNode = gridRef.current.api.getRowNode(getRowId({ data: delta }));
|
||||
if (rowNode) {
|
||||
const updatedData = produce<Positions_party_positions>(
|
||||
rowNode.data,
|
||||
(draft: Positions_party_positions) => {
|
||||
merge(draft, delta);
|
||||
}
|
||||
);
|
||||
if (updatedData !== rowNode.data) {
|
||||
update.push(updatedData);
|
||||
}
|
||||
} else {
|
||||
add.push(delta);
|
||||
}
|
||||
if (update.length || add.length) {
|
||||
gridRef.current.api.applyTransactionAsync({
|
||||
update,
|
||||
add,
|
||||
addIndex: 0,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
const dataRef = useRef<Position[] | null>(null);
|
||||
const update = useCallback(({ data }: { data: Data }) => {
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
dataRef.current = data.positions;
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
return true;
|
||||
}, []);
|
||||
const { data, error, loading } = useDataProvider<
|
||||
Positions_party_positions[],
|
||||
PositionSubscribe_positions
|
||||
>({ dataProvider: positionsDataProvider, update, variables });
|
||||
Data,
|
||||
PositionsMetricsSubscription_positions
|
||||
>({ dataProvider, update, variables });
|
||||
dataRef.current = data?.positions || null;
|
||||
const getRows = async ({
|
||||
successCallback,
|
||||
startRow,
|
||||
endRow,
|
||||
}: GetRowsParams) => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
<PositionsTable ref={gridRef} data={data} />
|
||||
<PositionsTable
|
||||
rowModelType={data?.positions?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.positions?.length ? undefined : []}
|
||||
datasource={{ getRows }}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
214
libs/positions/src/lib/positions-metrics-data-provider.spec.ts
Normal file
214
libs/positions/src/lib/positions-metrics-data-provider.spec.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import { AccountType, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import type { PositionsMetrics } from './__generated__/PositionsMetrics';
|
||||
import { getMetrics } from './positions-metrics-data-provider';
|
||||
|
||||
const data: PositionsMetrics = {
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
|
||||
accounts: [
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.General,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
decimals: 5,
|
||||
},
|
||||
balance: '892824769',
|
||||
market: null,
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.Margin,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
decimals: 5,
|
||||
},
|
||||
balance: '33353727',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Account',
|
||||
type: AccountType.Margin,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
decimals: 5,
|
||||
},
|
||||
balance: '3274050',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
},
|
||||
],
|
||||
marginsConnection: {
|
||||
__typename: 'MarginConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
symbol: 'tDAI',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'MarginEdge',
|
||||
node: {
|
||||
__typename: 'MarginLevels',
|
||||
maintenanceLevel: '0',
|
||||
searchLevel: '0',
|
||||
initialLevel: '0',
|
||||
collateralReleaseLevel: '0',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
symbol: 'tDAI',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
positionsConnection: {
|
||||
__typename: 'PositionConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'PositionEdge',
|
||||
node: {
|
||||
__typename: 'Position',
|
||||
openVolume: '100',
|
||||
averageEntryPrice: '8993727',
|
||||
updatedAt: '2022-07-28T14:53:54.725477Z',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '43804770',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
|
||||
decimalPlaces: 5,
|
||||
tradingMode: MarketTradingMode.MonitoringAuction,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
markPrice: '9431775',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'PositionEdge',
|
||||
node: {
|
||||
__typename: 'Position',
|
||||
openVolume: '-100',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '-9112700',
|
||||
averageEntryPrice: '840158',
|
||||
updatedAt: '2022-07-28T15:09:34.441143Z',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
decimalPlaces: 5,
|
||||
tradingMode: MarketTradingMode.Continuous,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
markPrice: '869762',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('getMetrics', () => {
|
||||
it('returns positions metrics', () => {
|
||||
const metrics = getMetrics(data.party);
|
||||
expect(metrics.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('calculates metrics', () => {
|
||||
const metrics = getMetrics(data.party);
|
||||
|
||||
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].assetDecimals).toEqual(5);
|
||||
expect(metrics[0].liquidationPrice).toEqual('169990');
|
||||
expect(metrics[0].lowMarginLevel).toEqual(false);
|
||||
expect(metrics[0].markPrice).toEqual('9431775');
|
||||
expect(metrics[0].marketId).toEqual(
|
||||
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
|
||||
);
|
||||
expect(metrics[0].marketName).toEqual('AAVEDAI Monthly (30 Jun 2022)');
|
||||
expect(metrics[0].marketTradingMode).toEqual('MonitoringAuction');
|
||||
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');
|
||||
|
||||
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].assetDecimals).toEqual(5);
|
||||
expect(metrics[1].liquidationPrice).toEqual('9830750');
|
||||
expect(metrics[1].lowMarginLevel).toEqual(false);
|
||||
expect(metrics[1].markPrice).toEqual('869762');
|
||||
expect(metrics[1].marketId).toEqual(
|
||||
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
|
||||
);
|
||||
expect(metrics[1].marketName).toEqual('UNIDAI Monthly (30 Jun 2022)');
|
||||
expect(metrics[1].marketTradingMode).toEqual('Continuous');
|
||||
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');
|
||||
});
|
||||
});
|
285
libs/positions/src/lib/positions-metrics-data-provider.ts
Normal file
285
libs/positions/src/lib/positions-metrics-data-provider.ts
Normal file
@ -0,0 +1,285 @@
|
||||
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
|
||||
);
|
@ -1,103 +1,176 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { RenderResult } from '@testing-library/react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import PositionsTable from './positions-table';
|
||||
import type { Positions_party_positions } from './__generated__/Positions';
|
||||
import type { Position } from './positions-metrics-data-provider';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
const singleRow: Positions_party_positions = {
|
||||
realisedPNL: '520000000',
|
||||
openVolume: '10000',
|
||||
unrealisedPNL: '895000',
|
||||
averageEntryPrice: '1129935',
|
||||
market: {
|
||||
id: 'b7010da9dbe7fbab2b74d9d5642fc4a8a0ca93ef803d21fa60c2cacd0416bba0',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
data: {
|
||||
markPrice: '1138885',
|
||||
marketTradingMode: MarketTradingMode.Continuous,
|
||||
__typename: 'MarketData',
|
||||
market: { __typename: 'Market', id: '123' },
|
||||
},
|
||||
positionDecimalPlaces: 2,
|
||||
decimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
id: '',
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
metadata: {
|
||||
tags: [
|
||||
'formerly:3C58ED2A4A6C5D7E',
|
||||
'base:UNI',
|
||||
'quote:DAI',
|
||||
'class:fx/crypto',
|
||||
'monthly',
|
||||
'sector:defi',
|
||||
],
|
||||
__typename: 'InstrumentMetadata',
|
||||
},
|
||||
code: 'UNIDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
|
||||
symbol: 'tDAI',
|
||||
name: 'tDAI TEST',
|
||||
decimals: 5,
|
||||
__typename: 'Asset',
|
||||
},
|
||||
quoteName: 'DAI',
|
||||
__typename: 'Future',
|
||||
},
|
||||
__typename: 'Instrument',
|
||||
},
|
||||
__typename: 'TradableInstrument',
|
||||
},
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'Position',
|
||||
const singleRow: Position = {
|
||||
marketName: 'ETH/BTC (31 july 2022)',
|
||||
averageEntryPrice: '133', // 13.3
|
||||
capitalUtilisation: 11, // 11.00%
|
||||
currentLeverage: 1.1,
|
||||
marketDecimalPlaces: 1,
|
||||
positionDecimalPlaces: 0,
|
||||
assetDecimals: 2,
|
||||
totalBalance: '123456',
|
||||
assetSymbol: 'BTC',
|
||||
liquidationPrice: '83', // 8.3
|
||||
lowMarginLevel: false,
|
||||
marketId: 'string',
|
||||
marketTradingMode: MarketTradingMode.Continuous,
|
||||
markPrice: '123', // 12.3
|
||||
notional: '12300', // 1230.0
|
||||
openVolume: '100', // 100
|
||||
realisedPNL: '123', // 1.23
|
||||
unrealisedPNL: '456', // 4.56
|
||||
searchPrice: '0',
|
||||
updatedAt: '2022-07-27T15:02:58.400Z',
|
||||
};
|
||||
|
||||
const singleRowData = [singleRow];
|
||||
|
||||
describe('PositionsTable', () => {
|
||||
it('should render successfully', async () => {
|
||||
act(async () => {
|
||||
const { baseElement } = render(<PositionsTable data={[]} />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render correct columns', async () => {
|
||||
act(async () => {
|
||||
render(<PositionsTable data={singleRowData} />);
|
||||
await waitFor(async () => {
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(5);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||
'Market',
|
||||
'Size',
|
||||
'Average Entry Price',
|
||||
'Mark Price',
|
||||
'Realised PNL',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply correct formatting', () => {
|
||||
act(async () => {
|
||||
render(<PositionsTable data={singleRowData} />);
|
||||
await waitFor(async () => {
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
singleRow.market.tradableInstrument.instrument.code,
|
||||
'+100.00',
|
||||
'11.29935',
|
||||
'11.38885',
|
||||
'+5,200.000',
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
expect(cells[cells.length - 1]).toHaveClass('text-vega-green-dark');
|
||||
});
|
||||
});
|
||||
it('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(<PositionsTable rowData={[]} />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('Render correct columns', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(9);
|
||||
expect(
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual([
|
||||
'Market',
|
||||
'Amount',
|
||||
'Mark Price',
|
||||
'Entry Price',
|
||||
'Leverage',
|
||||
'Margin allocated',
|
||||
'Realised PNL',
|
||||
'Unrealised PNL',
|
||||
'Updated',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Splits market name', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
expect(screen.getByText('ETH/BTC')).toBeTruthy();
|
||||
expect(screen.getByText('31 july 2022')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('add color and sign to amount, displays positive notional value', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
let values = cells[1].querySelectorAll('.text-right');
|
||||
expect(values[0].classList.contains('text-vega-green-dark')).toBeTruthy();
|
||||
expect(values[0].classList.contains('text-vega-red-dark')).toBeFalsy();
|
||||
expect(values[0].textContent).toEqual('+100');
|
||||
expect(values[1].textContent).toEqual('1,230.0');
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable rowData={[{ ...singleRow, openVolume: '-100' }]} />
|
||||
);
|
||||
});
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
values = cells[1].querySelectorAll('.text-right');
|
||||
expect(values[0].classList.contains('text-vega-green-dark')).toBeFalsy();
|
||||
expect(values[0].classList.contains('text-vega-red-dark')).toBeTruthy();
|
||||
expect(values[0].textContent?.startsWith('-100')).toBeTruthy();
|
||||
expect(values[1].textContent).toEqual('1,230.0');
|
||||
});
|
||||
|
||||
it('displays mark price', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[2].textContent).toEqual('12.3');
|
||||
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[
|
||||
{ ...singleRow, marketTradingMode: MarketTradingMode.OpeningAuction },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[2].textContent).toEqual('-');
|
||||
});
|
||||
|
||||
it("displays properly entry, liquidation price and liquidation bar and it's intent", async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
let cell = cells[3];
|
||||
const entryPrice = cell.firstElementChild?.firstElementChild?.textContent;
|
||||
const liquidationPrice =
|
||||
cell.firstElementChild?.lastElementChild?.textContent;
|
||||
const progressBarTrack = cell.lastElementChild;
|
||||
let progressBar = progressBarTrack?.firstElementChild as HTMLElement;
|
||||
const progressBarWidth = progressBar?.style?.width;
|
||||
expect(entryPrice).toEqual('13.3');
|
||||
expect(liquidationPrice).toEqual('8.3');
|
||||
expect(progressBar.classList.contains('bg-danger')).toEqual(false);
|
||||
expect(progressBarWidth).toEqual('20%');
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable rowData={[{ ...singleRow, lowMarginLevel: true }]} />
|
||||
);
|
||||
});
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
cell = cells[3];
|
||||
progressBar = cell.lastElementChild?.firstElementChild as HTMLElement;
|
||||
expect(progressBar?.classList.contains('bg-danger')).toEqual(true);
|
||||
});
|
||||
|
||||
it('displays leverage', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[4].textContent).toEqual('1.1');
|
||||
});
|
||||
|
||||
it('displays allocated margin and margin bar', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[5];
|
||||
const capitalUtilisation =
|
||||
cell.firstElementChild?.firstElementChild?.textContent;
|
||||
const totalBalance = cell.firstElementChild?.lastElementChild?.textContent;
|
||||
const progressBarTrack = cell.lastElementChild;
|
||||
const progressBar = progressBarTrack?.firstElementChild as HTMLElement;
|
||||
const progressBarWidth = progressBar?.style?.width;
|
||||
expect(capitalUtilisation).toEqual('11.00%');
|
||||
expect(totalBalance).toEqual('1,234.56');
|
||||
expect(progressBarWidth).toEqual('11%');
|
||||
});
|
||||
|
||||
it('displays realised and unrealised PNL', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[6].textContent).toEqual('1.23');
|
||||
expect(cells[7].textContent).toEqual('4.56');
|
||||
});
|
||||
|
80
libs/positions/src/lib/positions-table.stories.tsx
Normal file
80
libs/positions/src/lib/positions-table.stories.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { PositionsTable } from './positions-table';
|
||||
import type { Position } from './positions-metrics-data-provider';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
export default {
|
||||
component: PositionsTable,
|
||||
title: 'PositionsTable',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => <PositionsTable {...args} />;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
const longPosition: Position = {
|
||||
marketName: 'BTC/USD (31 july 2022)',
|
||||
averageEntryPrice: '1134564',
|
||||
capitalUtilisation: 10.0,
|
||||
currentLeverage: 11,
|
||||
assetDecimals: 2,
|
||||
marketDecimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
// generalAccountBalance: '0',
|
||||
totalBalance: '45353',
|
||||
assetSymbol: 'BTC',
|
||||
// leverageInitial: '0',
|
||||
// leverageMaintenance: '0',
|
||||
// leverageRelease: '0',
|
||||
// leverageSearch: '0',
|
||||
liquidationPrice: '1129935',
|
||||
lowMarginLevel: false,
|
||||
// marginAccountBalance: '0',
|
||||
// marginMaintenance: '0',
|
||||
// marginSearch: '0',
|
||||
// marginInitial: '0',
|
||||
marketId: 'marketId1',
|
||||
marketTradingMode: MarketTradingMode.Continuous,
|
||||
markPrice: '1131894',
|
||||
notional: '46667989',
|
||||
openVolume: '4123',
|
||||
realisedPNL: '45',
|
||||
unrealisedPNL: '45',
|
||||
searchPrice: '1132123',
|
||||
updatedAt: '2022-07-27T15:02:58.400Z',
|
||||
};
|
||||
|
||||
const shortPosition: Position = {
|
||||
marketName: 'ETH/USD (31 august 2022)',
|
||||
averageEntryPrice: '23976',
|
||||
capitalUtilisation: 87.0,
|
||||
currentLeverage: 7,
|
||||
assetDecimals: 2,
|
||||
marketDecimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
// generalAccountBalance: '0',
|
||||
totalBalance: '3856',
|
||||
assetSymbol: 'ETH',
|
||||
// leverageInitial: '0',
|
||||
// leverageMaintenance: '0',
|
||||
// leverageRelease: '0',
|
||||
// leverageSearch: '0',
|
||||
liquidationPrice: '23734',
|
||||
lowMarginLevel: false,
|
||||
// marginAccountBalance: '0',
|
||||
// marginMaintenance: '0',
|
||||
// marginSearch: '0',
|
||||
// marginInitial: '0',
|
||||
marketId: 'marketId2',
|
||||
marketTradingMode: MarketTradingMode.Continuous,
|
||||
markPrice: '24123',
|
||||
notional: '836344',
|
||||
openVolume: '-3467',
|
||||
realisedPNL: '0',
|
||||
unrealisedPNL: '0',
|
||||
searchPrice: '0',
|
||||
updatedAt: '2022-07-26T14:01:34.800Z',
|
||||
};
|
||||
|
||||
Primary.args = {
|
||||
rowData: [longPosition, shortPosition],
|
||||
};
|
@ -1,186 +1,343 @@
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef } from 'react';
|
||||
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||
import {
|
||||
PriceFlashCell,
|
||||
addDecimalsFormatNumber,
|
||||
volumePrefix,
|
||||
addDecimal,
|
||||
t,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AgGridDynamic as AgGrid, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Positions_party_positions } from './__generated__/Positions';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
|
||||
import type { Position } from './positions-metrics-data-provider';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface PositionsTableProps {
|
||||
data: Positions_party_positions[] | null;
|
||||
export const getRowId = ({ data }: { data: Position }) => data.marketId;
|
||||
|
||||
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
|
||||
successCallback(rowsThisBlock: Position[], lastRow?: number): void;
|
||||
}
|
||||
|
||||
type ColumnHeaderProps = {
|
||||
displayName: string;
|
||||
tooltipContent?: string;
|
||||
};
|
||||
|
||||
const ColumnHeader = ({ displayName, tooltipContent }: ColumnHeaderProps) => {
|
||||
if (tooltipContent) {
|
||||
return (
|
||||
<Tooltip description={tooltipContent}>
|
||||
<span>{displayName}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return displayName;
|
||||
};
|
||||
|
||||
export const getRowId = ({ data }: { data: Positions_party_positions }) =>
|
||||
data.market.id;
|
||||
|
||||
const alphanumericComparator = (a: string, b: string, isInverted: boolean) => {
|
||||
if (a < b) {
|
||||
return isInverted ? 1 : -1;
|
||||
}
|
||||
|
||||
if (a > b) {
|
||||
return isInverted ? -1 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const comparator = (
|
||||
valueA: string,
|
||||
valueB: string,
|
||||
nodeA: { data: Positions_party_positions },
|
||||
nodeB: { data: Positions_party_positions },
|
||||
isInverted: boolean
|
||||
) =>
|
||||
alphanumericComparator(
|
||||
nodeA.data.market.tradableInstrument.instrument.name,
|
||||
nodeB.data.market.tradableInstrument.instrument.name,
|
||||
isInverted
|
||||
) ||
|
||||
alphanumericComparator(
|
||||
nodeA.data.market.id,
|
||||
nodeB.data.market.id,
|
||||
isInverted
|
||||
);
|
||||
|
||||
interface PositionsTableValueFormatterParams extends ValueFormatterParams {
|
||||
data: Positions_party_positions;
|
||||
export interface Datasource extends IDatasource {
|
||||
getRows(params: GetRowsParams): void;
|
||||
}
|
||||
interface Props extends AgGridReactProps {
|
||||
rowData?: Position[] | null;
|
||||
datasource?: Datasource;
|
||||
}
|
||||
|
||||
export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
|
||||
({ data }, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate="No positions"
|
||||
rowData={data}
|
||||
getRowId={getRowId}
|
||||
ref={ref}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
}}
|
||||
onGridReady={(event) => {
|
||||
event.columnApi.applyColumnState({
|
||||
state: [
|
||||
{
|
||||
colId: 'market.tradableInstrument.instrument.code',
|
||||
sort: 'asc',
|
||||
},
|
||||
],
|
||||
});
|
||||
}}
|
||||
components={{ PriceFlashCell, agColumnHeader: ColumnHeader }}
|
||||
type PositionsTableValueFormatterParams = Omit<
|
||||
ValueFormatterParams,
|
||||
'data' | 'value'
|
||||
> & {
|
||||
data: Position;
|
||||
};
|
||||
|
||||
export interface MarketNameCellProps {
|
||||
valueFormatted?: [string, string];
|
||||
}
|
||||
|
||||
export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
|
||||
return valueFormatted ? (
|
||||
<div className="leading-tight">
|
||||
<div>{valueFormatted[0]}</div>
|
||||
{valueFormatted[1] ? <div>{valueFormatted[1]}</div> : null}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export interface PriceCellProps {
|
||||
valueFormatted?: {
|
||||
low: string;
|
||||
high: string;
|
||||
value: number;
|
||||
intent?: Intent;
|
||||
};
|
||||
}
|
||||
|
||||
export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
|
||||
return valueFormatted ? (
|
||||
<>
|
||||
<div className="flex justify-between leading-tight">
|
||||
<div>{valueFormatted.low}</div>
|
||||
<div>{valueFormatted.high}</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={valueFormatted.value}
|
||||
intent={valueFormatted.intent}
|
||||
className="mt-4"
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
ProgressBarCell.displayName = 'PriceFlashCell';
|
||||
|
||||
export interface AmountCellProps {
|
||||
valueFormatted?: Pick<
|
||||
Position,
|
||||
'openVolume' | 'marketDecimalPlaces' | 'positionDecimalPlaces' | 'notional'
|
||||
>;
|
||||
}
|
||||
|
||||
export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
|
||||
if (!valueFormatted) {
|
||||
return null;
|
||||
}
|
||||
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
|
||||
valueFormatted;
|
||||
const isShortPosition = openVolume.startsWith('-');
|
||||
return valueFormatted ? (
|
||||
<div className="leading-tight">
|
||||
<div
|
||||
className={classNames('text-right', {
|
||||
'text-vega-green-dark dark:text-vega-green': !isShortPosition,
|
||||
'text-vega-red-dark dark:text-vega-red': isShortPosition,
|
||||
})}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="market.tradableInstrument.instrument.code"
|
||||
comparator={comparator}
|
||||
sortable
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Size')}
|
||||
field="openVolume"
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => Number(value) > 0,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => Number(value) < 0,
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams) =>
|
||||
volumePrefix(addDecimal(value, data.market.positionDecimalPlaces))
|
||||
{volumePrefix(
|
||||
addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{addDecimalsFormatNumber(notional, marketDecimalPlaces)}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
AmountCell.displayName = 'AmountCell';
|
||||
|
||||
export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate="No positions"
|
||||
getRowId={getRowId}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
}}
|
||||
components={{ PriceFlashCell, ProgressBarCell }}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketName"
|
||||
cellRenderer={MarketNameCell}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['marketName'];
|
||||
}) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Average Entry Price')}
|
||||
field="averageEntryPrice"
|
||||
cellRenderer="PriceFlashCell"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams) =>
|
||||
addDecimalsFormatNumber(value, data.market.decimalPlaces)
|
||||
// split market name into two parts, 'Part1 (Part2)'
|
||||
const matches = value.match(/^(.*)\((.*)\)\s*$/);
|
||||
if (matches) {
|
||||
return [matches[1].trim(), matches[2].trim()];
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark Price')}
|
||||
field="market.data.markPrice"
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams) => {
|
||||
if (
|
||||
data.market.data?.marketTradingMode ===
|
||||
MarketTradingMode.OpeningAuction
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimal(value, data.market.decimalPlaces);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Realised PNL')}
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
headerComponentParams={{
|
||||
tooltipContent: t('P&L excludes any fees paid.'),
|
||||
}}
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => Number(value) > 0,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => Number(value) < 0,
|
||||
}}
|
||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
||||
volumePrefix(
|
||||
addDecimalsFormatNumber(value, data.market.decimalPlaces, 3)
|
||||
)
|
||||
return [value];
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Amount')}
|
||||
field="openVolume"
|
||||
type="rightAligned"
|
||||
cellRenderer={AmountCell}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['openVolume'];
|
||||
}): AmountCellProps['valueFormatted'] => {
|
||||
if (!value || !data) {
|
||||
return undefined;
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark Price')}
|
||||
field="markPrice"
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['markPrice'];
|
||||
}) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if (data.marketTradingMode === MarketTradingMode.OpeningAuction) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
value.toString(),
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Entry Price')}
|
||||
field="averageEntryPrice"
|
||||
headerComponentParams={{
|
||||
template:
|
||||
'<div class="ag-cell-label-container" role="presentation">' +
|
||||
` <span>${t('Liquidation price (est)')}</span>` +
|
||||
' <span ref="eText" class="ag-header-cell-text"></span>' +
|
||||
'</div>',
|
||||
}}
|
||||
flex={2}
|
||||
cellRenderer="ProgressBarCell"
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams):
|
||||
| PriceCellProps['valueFormatted']
|
||||
| undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.averageEntryPrice);
|
||||
const max = BigInt(data.liquidationPrice);
|
||||
const mid = BigInt(data.markPrice);
|
||||
const range = max - min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(
|
||||
min.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
high: addDecimalsFormatNumber(
|
||||
max.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
|
||||
intent: data.lowMarginLevel ? Intent.Danger : undefined,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Leverage')}
|
||||
field="currentLeverage"
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['currentLeverage'];
|
||||
}) =>
|
||||
value === undefined ? undefined : formatNumber(value.toString(), 1)
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Margin allocated')}
|
||||
field="capitalUtilisation"
|
||||
type="rightAligned"
|
||||
flex={2}
|
||||
cellRenderer="ProgressBarCell"
|
||||
valueFormatter={({
|
||||
data,
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['capitalUtilisation'];
|
||||
}): PriceCellProps['valueFormatted'] | undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
low: `${formatNumber(value, 2)}%`,
|
||||
high: addDecimalsFormatNumber(
|
||||
data.totalBalance,
|
||||
data.assetDecimals
|
||||
),
|
||||
value: Number(value),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Realised PNL')}
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => BigInt(value) > 0,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => BigInt(value) < 0,
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['realisedPNL'];
|
||||
}) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.assetDecimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
headerTooltip={t('P&L excludes any fees paid.')}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => BigInt(value) > 0,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => BigInt(value) < 0,
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['unrealisedPNL'];
|
||||
}) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.assetDecimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
field="updatedAt"
|
||||
type="rightAligned"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['updatedAt'];
|
||||
}) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
});
|
||||
|
||||
export default PositionsTable;
|
||||
|
17
libs/positions/tailwind.config.js
Normal file
17
libs/positions/tailwind.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
const { join } = require('path');
|
||||
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
|
||||
const theme = require('../tailwindcss-config/src/theme');
|
||||
const vegaCustomClasses = require('../tailwindcss-config/src/vega-custom-classes');
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
join(__dirname, 'src/**/*.{ts,tsx,html,mdx}'),
|
||||
join(__dirname, '.storybook/preview.js'),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: theme,
|
||||
},
|
||||
plugins: [vegaCustomClasses],
|
||||
};
|
@ -20,6 +20,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./.storybook/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -16,7 +16,11 @@
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx"
|
||||
"**/*.test.jsx",
|
||||
"**/*.stories.ts",
|
||||
"**/*.stories.js",
|
||||
"**/*.stories.jsx",
|
||||
"**/*.stories.tsx"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export * from './loader';
|
||||
export * from './lozenge';
|
||||
export * from './popover';
|
||||
export * from './price-change';
|
||||
export * from './progress-bar';
|
||||
export * from './radio-group';
|
||||
export * from './resizable-panel';
|
||||
export * from './select';
|
||||
|
1
libs/ui-toolkit/src/components/progress-bar/index.ts
Normal file
1
libs/ui-toolkit/src/components/progress-bar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './progress-bar';
|
@ -0,0 +1,10 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { ProgressBar } from './progress-bar';
|
||||
|
||||
describe('Progress Bar', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<ProgressBar />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { ProgressBar } from './progress-bar';
|
||||
import { Intent } from '../../utils/intent';
|
||||
|
||||
export default {
|
||||
component: ProgressBar,
|
||||
title: 'ProgressBar',
|
||||
storySort: {
|
||||
order: ['Default', 'None', 'Primary', 'Danger', 'Warning', 'Success'],
|
||||
},
|
||||
argTypes: {
|
||||
intent: {
|
||||
options: Object.values(Intent).filter((x) => typeof x === 'string'),
|
||||
mapping: Intent,
|
||||
control: {
|
||||
type: 'radio',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => <ProgressBar {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
value: 10,
|
||||
};
|
||||
|
||||
export const None = Template.bind({});
|
||||
None.args = {
|
||||
intent: 'None',
|
||||
value: 10,
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
variant: 'Primary',
|
||||
value: 10,
|
||||
};
|
||||
|
||||
export const Success = Template.bind({});
|
||||
Success.args = {
|
||||
intent: 'Success',
|
||||
value: 30,
|
||||
};
|
||||
|
||||
export const Warning = Template.bind({});
|
||||
Warning.args = {
|
||||
intent: 'Warning',
|
||||
value: 40,
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
intent: 'Danger',
|
||||
value: 50,
|
||||
};
|
28
libs/ui-toolkit/src/components/progress-bar/progress-bar.tsx
Normal file
28
libs/ui-toolkit/src/components/progress-bar/progress-bar.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import classNames from 'classnames';
|
||||
import { getIntentBackground } from '../../utils/intent';
|
||||
import { Intent } from '../../utils/intent';
|
||||
|
||||
interface ProgressBarProps {
|
||||
value?: number;
|
||||
intent?: Intent;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProgressBar = ({ className, intent, value }: ProgressBarProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{ height: '6px' }}
|
||||
className={classNames('bg-black-10 relative', className)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute left-0 top-0 bottom-0',
|
||||
intent === undefined || intent === Intent.None
|
||||
? 'bg-black-60'
|
||||
: getIntentBackground(intent ?? Intent.None)
|
||||
)}
|
||||
style={{ width: `${Math.max(0, value ?? 0)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -28,13 +28,25 @@ export const getIntentBorder = (intent = Intent.None) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getIntentTextAndBackground = (intent?: Intent) => {
|
||||
export const getIntentBackground = (intent?: Intent) => {
|
||||
return {
|
||||
'bg-black text-white dark:bg-white dark:text-black': intent === Intent.None,
|
||||
'bg-vega-pink text-black dark:bg-vega-yellow dark:text-black-normal':
|
||||
intent === Intent.Primary,
|
||||
'bg-danger text-white': intent === Intent.Danger,
|
||||
'bg-warning text-black': intent === Intent.Warning,
|
||||
'bg-success text-black': intent === Intent.Success,
|
||||
'bg-black dark:bg-white': intent === Intent.None,
|
||||
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
|
||||
'bg-danger': intent === Intent.Danger,
|
||||
'bg-warning': intent === Intent.Warning,
|
||||
'bg-success': intent === Intent.Success,
|
||||
};
|
||||
};
|
||||
|
||||
export const getIntentText = (intent?: Intent) => {
|
||||
return {
|
||||
'text-white dark:text-black': intent === Intent.None,
|
||||
'text-black dark:text-black-normal': intent === Intent.Primary,
|
||||
'text-white': intent === Intent.Danger,
|
||||
'text-black': intent === Intent.Warning || intent === Intent.Success,
|
||||
};
|
||||
};
|
||||
|
||||
export const getIntentTextAndBackground = (intent?: Intent) => {
|
||||
return { ...getIntentText(intent), ...getIntentBackground(intent) };
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user