Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe8de91233 | ||
|
|
3772b624d0 |
@@ -11,7 +11,6 @@ import { TxDetailsBatch } from './tx-batch';
|
||||
import { TxDetailsChainEvent } from './tx-chain-event';
|
||||
import { TxDetailsNodeVote } from './tx-node-vote';
|
||||
import { TxDetailsOrderCancel } from './tx-order-cancel';
|
||||
import { TxDetailsStopOrderCancel } from './tx-stop-order-cancel';
|
||||
import { TxDetailsOrderAmend } from './tx-order-amend';
|
||||
import { TxDetailsWithdrawSubmission } from './tx-withdraw-submission';
|
||||
import { TxDetailsDelegate } from './tx-delegation';
|
||||
@@ -86,8 +85,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsProtocolUpgrade;
|
||||
case 'Cancel Order':
|
||||
return TxDetailsOrderCancel;
|
||||
case 'Stop Orders Cancellation':
|
||||
return TxDetailsStopOrderCancel;
|
||||
case 'Amend Order':
|
||||
return TxDetailsOrderAmend;
|
||||
case 'Validator Heartbeat':
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import { MarketLink } from '../../links/';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import { CancelSummary } from '../../order-summary/order-cancellation';
|
||||
import Hash from '../../links/hash';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
|
||||
export type StopOrderCancellationTransaction =
|
||||
components['schemas']['v1StopOrdersCancellation'];
|
||||
|
||||
interface TxDetailsStopOrderCancelProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Someone cancelled a stop order
|
||||
*/
|
||||
export const TxDetailsStopOrderCancel = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsStopOrderCancelProps) => {
|
||||
if (!txData || !txData.command) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const command: StopOrderCancellationTransaction =
|
||||
txData.command.stopOrdersCancellation;
|
||||
|
||||
const { marketId, stopOrderId } = command;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Cancel stop order')}</TableCell>
|
||||
<TableCell>
|
||||
{stopOrderId ? (
|
||||
<Hash text={stopOrderId} />
|
||||
) : (
|
||||
<CancelSummary orderId={stopOrderId} marketId={marketId} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{marketId ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Market')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink id={marketId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,6 @@ export type FilterOption =
|
||||
| 'Protocol Upgrade'
|
||||
| 'Register new Node'
|
||||
| 'State Variable Proposal'
|
||||
| 'Stop Orders Cancellation'
|
||||
| 'Submit Oracle Data'
|
||||
| 'Submit Order'
|
||||
| 'Transfer Funds'
|
||||
@@ -54,7 +53,6 @@ export const PrimaryFilterOptions: FilterOption[] = [
|
||||
'Delegate',
|
||||
'Liquidity Provision Order',
|
||||
'Proposal',
|
||||
'Stop Orders Cancellation',
|
||||
'Submit Oracle Data',
|
||||
'Submit Order',
|
||||
'Transfer Funds',
|
||||
|
||||
@@ -626,8 +626,7 @@
|
||||
"status-tendermint": "Consensus",
|
||||
"status-ersatz": "Standby",
|
||||
"status-pending": "Candidate",
|
||||
"ersatzDescription1": "To be promoted, a standby validator must have more than the lowest consensus stake, plus a bonus given to existing validators, and only one standby validator can be promoted per epoch. Currently this requires a minimum of",
|
||||
"ersatzDescription2": "stake assuming no performance penalty incurred.",
|
||||
"ersatzDescription": "To be promoted, a standby validator must have more than the lowest consensus stake, plus a bonus given to existing validators. This currently requires a minimum of {{stakeNeededForPromotion}} stake assuming no penalties. Only one validator per epoch can be promoted.",
|
||||
"pendingDescription1": "Anyone can",
|
||||
"pendingDescriptionLinkText": "set up and run a node on Vega",
|
||||
"pendingDescription2": ". A node can move from being a candidate into standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.",
|
||||
@@ -785,7 +784,6 @@
|
||||
"PerformancePenaltyDescription": "Performance score is a measure of how often a validator proposed blocks in the last epoch relative to how many they should be expected to propose based on their voting power. Performance penalty is applied for having a performance score of less than 1",
|
||||
"UnnormalisedVotingPowerDescription": "The voting power of the validator based on their final validator score after all penalties have been applied",
|
||||
"NormalisedVotingPowerDescription": "The voting power of the validator, adjusted to ensure all validator scores sum to 1, used for distribution of rewards",
|
||||
"NonConsensusVotingPowerDescription": "The voting power of the validator. Only consensus validators have voting power",
|
||||
"Score": "Score",
|
||||
"performancePenalty": "Performance penalty",
|
||||
"overstaked": "Overstaked",
|
||||
|
||||
+2
-2
@@ -389,10 +389,10 @@ export const ConsensusValidatorsTable = ({
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.NORMALISED_VOTING_POWER,
|
||||
headerName: t('votingPower').toString(),
|
||||
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
|
||||
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
|
||||
cellRenderer: VotingPowerRenderer,
|
||||
width: 120,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.TOTAL_PENALTIES,
|
||||
|
||||
+2
-11
@@ -20,7 +20,6 @@ import {
|
||||
TotalStakeRenderer,
|
||||
StakeShareRenderer,
|
||||
PendingStakeRenderer,
|
||||
VotingPowerRenderer,
|
||||
} from './shared';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
@@ -40,6 +39,7 @@ interface StandbyPendingValidatorsTableProps extends ValidatorsTableProps {
|
||||
export const StandbyPendingValidatorsTable = ({
|
||||
data,
|
||||
previousEpochData,
|
||||
totalStake,
|
||||
stakeNeededForPromotion,
|
||||
stakeNeededForPromotionDescription,
|
||||
validatorsView,
|
||||
@@ -132,8 +132,6 @@ export const StandbyPendingValidatorsTable = ({
|
||||
name,
|
||||
},
|
||||
[ValidatorFields.STAKE]: stakedTotal,
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]: '0%',
|
||||
[ValidatorFields.UNNORMALISED_VOTING_POWER]: '0%',
|
||||
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
|
||||
individualStakeNeededForPromotion || null,
|
||||
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION]:
|
||||
@@ -225,14 +223,7 @@ export const StandbyPendingValidatorsTable = ({
|
||||
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
|
||||
headerTooltip: t('StakeShareDescription').toString(),
|
||||
cellRenderer: StakeShareRenderer,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: ValidatorFields.NORMALISED_VOTING_POWER,
|
||||
headerName: t('votingPower').toString(),
|
||||
headerTooltip: t('NonConsensusVotingPowerDescription').toString(),
|
||||
cellRenderer: VotingPowerRenderer,
|
||||
width: 120,
|
||||
width: 100,
|
||||
},
|
||||
// {
|
||||
// field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
|
||||
|
||||
@@ -17,10 +17,6 @@ import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
|
||||
import type { StakingQuery } from '../../__generated__/Staking';
|
||||
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
|
||||
import type { ValidatorWithUserData } from './shared';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
|
||||
export interface ValidatorsTableProps {
|
||||
nodesData: NodesQuery | undefined;
|
||||
@@ -45,9 +41,6 @@ export const ValidatorTables = ({
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.network_validators_incumbentBonus,
|
||||
]);
|
||||
|
||||
const [validatorsView, setValidatorsView] = useState<ValidatorsView>('all');
|
||||
const totalStake = useMemo(
|
||||
@@ -59,10 +52,6 @@ export const ValidatorTables = ({
|
||||
() => userStakingData?.party?.stakingSummary.currentStakeAvailable || '0',
|
||||
[userStakingData?.party?.stakingSummary.currentStakeAvailable]
|
||||
);
|
||||
const incumbentBonus = useMemo(
|
||||
() => new BigNumber(params?.network_validators_incumbentBonus),
|
||||
[params?.network_validators_incumbentBonus]
|
||||
);
|
||||
|
||||
let stakeNeededForPromotion = undefined;
|
||||
let delegations: StakingDelegationFieldsFragment[] | undefined = undefined;
|
||||
@@ -137,23 +126,28 @@ export const ValidatorTables = ({
|
||||
consensusValidators.length &&
|
||||
(standbyValidators.length || pendingValidators.length)
|
||||
) {
|
||||
const lowestConsensusStake = consensusValidators.reduce(
|
||||
const lowestRankingConsensusScore = consensusValidators.reduce(
|
||||
(lowest: ValidatorWithUserData, validator: ValidatorWithUserData) => {
|
||||
if (Number(validator.stakedTotal) < Number(lowest.stakedTotal)) {
|
||||
if (
|
||||
Number(validator.rankingScore.rankingScore) <
|
||||
Number(lowest.rankingScore.rankingScore)
|
||||
) {
|
||||
lowest = validator;
|
||||
}
|
||||
return lowest;
|
||||
}
|
||||
).stakedTotal;
|
||||
).rankingScore.rankingScore;
|
||||
|
||||
const lowestRankingBigNum = toBigNum(lowestConsensusStake, decimals);
|
||||
const lowestRankingBigNum = toBigNum(lowestRankingConsensusScore, 0);
|
||||
const consensusStakedTotal = consensusValidators.reduce((acc, cur) => {
|
||||
return acc.plus(toBigNum(cur.stakedTotal, decimals));
|
||||
}, new BigNumber(0));
|
||||
|
||||
stakeNeededForPromotion = formatNumber(
|
||||
lowestRankingBigNum.multipliedBy(incumbentBonus.plus(1)),
|
||||
lowestRankingBigNum.times(consensusStakedTotal),
|
||||
2
|
||||
).toString();
|
||||
}
|
||||
|
||||
return (
|
||||
<section data-testid="validator-tables">
|
||||
<div className="grid w-full justify-end">
|
||||
@@ -193,18 +187,12 @@ export const ValidatorTables = ({
|
||||
<div className="mb-10">
|
||||
<SubHeading title={t('status-ersatz')} />
|
||||
<p>
|
||||
<span>
|
||||
<Trans i18nKey="ersatzDescription1" />
|
||||
</span>
|
||||
|
||||
<span className="text-white font-bold">
|
||||
{' '}
|
||||
{stakeNeededForPromotion}{' '}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<Trans i18nKey="ersatzDescription2" />
|
||||
</span>
|
||||
<Trans
|
||||
i18nKey="ersatzDescription"
|
||||
values={{
|
||||
stakeNeededForPromotion,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<StandbyPendingValidatorsTable
|
||||
data={standbyValidators}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const calculateOverstakedPenalty = (nodeId: string, nodes: Node[]) => {
|
||||
}
|
||||
const penalty = new BigNumber(1)
|
||||
.minus(
|
||||
new BigNumber(node.rewardScore?.rawValidatorScore || 1).dividedBy(tts)
|
||||
new BigNumber(node.rewardScore?.rawValidatorScore || 0).dividedBy(tts)
|
||||
)
|
||||
.times(100);
|
||||
return penalty.isLessThan(0) ? new BigNumber(0) : penalty;
|
||||
|
||||
@@ -447,7 +447,8 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[data-testid="icon-edit"]')
|
||||
.find('[data-testid="edit"]')
|
||||
.should('have.text', 'Edit')
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click();
|
||||
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
|
||||
@@ -475,7 +476,8 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="icon-cross"]`)
|
||||
.find(`[data-testid="cancel"]`)
|
||||
.should('have.text', 'Cancel')
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click({ force: true });
|
||||
const order: OrderCancellation = {
|
||||
@@ -512,7 +514,8 @@ describe('amend and cancel order', { tags: '@smoke' }, () => {
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[data-testid="icon-edit"]')
|
||||
.find('[data-testid="edit"]')
|
||||
.should('have.text', 'Edit')
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click({ force: true });
|
||||
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
import { checkSorting, aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { marketsDataQuery } from '@vegaprotocol/mock';
|
||||
import { positionsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
// #region consts
|
||||
const closePosition = 'close-position';
|
||||
const dialogCloseX = 'dialog-close';
|
||||
const dialogContent = 'dialog-content';
|
||||
const dropDownMenu = 'dropdown-menu';
|
||||
const marketActionsContent = 'position-actions-content';
|
||||
const positions = 'Positions';
|
||||
const tabPositions = 'tab-positions';
|
||||
const toastContent = 'toast-content';
|
||||
const tooltipContent = 'tooltip-content';
|
||||
// #endregion
|
||||
|
||||
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
it('renders positions on trading page', () => {
|
||||
visitAndClickPositions();
|
||||
// 7004-POSI-001
|
||||
// 7004-POSI-002
|
||||
validatePositionsDisplayed();
|
||||
});
|
||||
|
||||
// TODO: move this to sim, its flakey
|
||||
it.skip('renders positions on portfolio page', () => {
|
||||
cy.mockGQL((req) => {
|
||||
const positions = positionsQuery();
|
||||
if (positions.positions?.edges) {
|
||||
positions.positions.edges.push(
|
||||
...positions.positions.edges.map((edge) => ({
|
||||
...edge,
|
||||
node: {
|
||||
...edge.node,
|
||||
party: {
|
||||
...edge.node.party,
|
||||
id: 'vega-1',
|
||||
},
|
||||
},
|
||||
}))
|
||||
);
|
||||
}
|
||||
aliasGQLQuery(req, 'Positions', positions);
|
||||
});
|
||||
visitAndClickPositions();
|
||||
// 7004-POSI-001
|
||||
// 7004-POSI-002
|
||||
validatePositionsDisplayed(true);
|
||||
});
|
||||
|
||||
it('Close my position', () => {
|
||||
visitAndClickPositions();
|
||||
cy.getByTestId(closePosition).first().click();
|
||||
// 7004-POSI-010
|
||||
cy.getByTestId(toastContent).should(
|
||||
'contain.text',
|
||||
'Awaiting confirmation'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('positions', { tags: '@regression', testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('rows should be displayed despite errors', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['marketsConnection', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
},
|
||||
},
|
||||
];
|
||||
const marketData = marketsDataQuery();
|
||||
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||
const replace =
|
||||
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||
return { ...market, node: { ...market.node, data: replace } };
|
||||
});
|
||||
const overrides = {
|
||||
...marketData,
|
||||
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
const emptyCells = [
|
||||
'notional',
|
||||
'markPrice',
|
||||
'currentLeverage',
|
||||
'averageEntryPrice',
|
||||
];
|
||||
cy.getByTestId(tabPositions)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(
|
||||
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
|
||||
)
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
emptyCells.forEach((cell) => {
|
||||
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('error message should be displayed', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['assets', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
},
|
||||
},
|
||||
];
|
||||
const overrides = {
|
||||
marketsConnection: { edges: [] },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(tabPositions).contains('no market data');
|
||||
});
|
||||
|
||||
it('sorting by Market', () => {
|
||||
visitAndClickPositions();
|
||||
const marketsSortedDefault = [
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
const marketsSortedAsc = [
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
const marketsSortedDesc = [
|
||||
'SOLUSD',
|
||||
'ETHBTC.QM21',
|
||||
'BTCUSD.MF21',
|
||||
'AAPL.MF21',
|
||||
];
|
||||
cy.getByTestId(positions).click();
|
||||
// 7004-POSI-003
|
||||
checkSorting(
|
||||
'marketName',
|
||||
marketsSortedDefault,
|
||||
marketsSortedAsc,
|
||||
marketsSortedDesc,
|
||||
' [data-testid="market-code"]'
|
||||
);
|
||||
});
|
||||
|
||||
// let elementWidth: number;
|
||||
|
||||
it('Resize column', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.find('.ag-header-cell-resize')
|
||||
.realMouseDown()
|
||||
.realMouseMove(250, 0)
|
||||
.realMouseUp();
|
||||
});
|
||||
|
||||
// 7004-POSI-006
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.invoke('width')
|
||||
.should('be.greaterThan', 250);
|
||||
});
|
||||
|
||||
// This test depends on the previous one
|
||||
it('Has persisted column widths', () => {
|
||||
const width = 400;
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem(
|
||||
'vega_positions_store',
|
||||
JSON.stringify({
|
||||
state: {
|
||||
gridStore: {
|
||||
columnState: [{ colId: 'marketName', width }],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
visitAndClickPositions();
|
||||
|
||||
// 7004-POSI-012
|
||||
cy.get('.ag-center-cols-container .ag-row')
|
||||
.first()
|
||||
.find('[col-id="marketName"]')
|
||||
.invoke('outerWidth')
|
||||
.should('equal', width);
|
||||
});
|
||||
|
||||
it('Scroll horizontally', () => {
|
||||
visitAndClickPositions();
|
||||
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get(`[col-id="marketName"]`)
|
||||
.find('.ag-header-cell-resize')
|
||||
.realMouseDown()
|
||||
.realMouseMove(400, 0)
|
||||
.realMouseUp();
|
||||
});
|
||||
cy.get('[col-id="marketName"]').should('be.visible');
|
||||
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
|
||||
deltaX: 500,
|
||||
});
|
||||
// 7004-POSI-004
|
||||
cy.get('[col-id="unrealisedPNL"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('Drag and drop columns', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
|
||||
cy.get('[col-id="marketName"]')
|
||||
.realMouseDown()
|
||||
.realMouseMove(700, 15)
|
||||
.realMouseUp();
|
||||
|
||||
// 7004-POSI-005
|
||||
cy.get('[col-id="marketName"]').should(($element) => {
|
||||
const attributeValue = $element.attr('aria-colindex');
|
||||
expect(attributeValue).not.to.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
it('I can see warnings', () => {
|
||||
visitAndClickPositions();
|
||||
|
||||
cy.get('[col-id="openVolume"]')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('[aria-label="warning-sign icon"]')
|
||||
.should('be.visible')
|
||||
.realHover();
|
||||
});
|
||||
// 7004-POSI-011
|
||||
cy.getByTestId(tooltipContent).should('be.visible');
|
||||
});
|
||||
|
||||
it('Positive and Negative color change', () => {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(positions).click();
|
||||
// 7004-POSI-007
|
||||
cy.get('.ag-center-cols-container').within(() => {
|
||||
assertPNLColor(
|
||||
'[col-id="realisedPNL"]',
|
||||
'text-market-green-600',
|
||||
'text-market-red'
|
||||
);
|
||||
});
|
||||
cy.get('.ag-center-cols-container').within(() => {
|
||||
assertPNLColor(
|
||||
'[col-id="unrealisedPNL"]',
|
||||
'text-market-green-600',
|
||||
'text-market-red'
|
||||
);
|
||||
});
|
||||
cy.get('.ag-center-cols-container').within(() => {
|
||||
assertPNLColor(
|
||||
'[col-id="openVolume"]',
|
||||
'text-market-green-600',
|
||||
'text-market-red'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('View settlement asset', () => {
|
||||
visitAndClickPositions();
|
||||
cy.get('[col-id="asset"]')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('button[type="button"]').click();
|
||||
});
|
||||
// 7004-POSI-008
|
||||
cy.getByTestId(dialogContent).should('be.visible');
|
||||
cy.getByTestId(dialogCloseX).click();
|
||||
cy.getByTestId(dropDownMenu).first().click();
|
||||
cy.getByTestId(marketActionsContent).click();
|
||||
// 7004-POSI-009
|
||||
cy.getByTestId(dialogContent).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
function validatePositionsDisplayed(multiKey = false) {
|
||||
cy.getByTestId('tab-positions').should('be.visible');
|
||||
cy.getByTestId('tab-positions')
|
||||
.get('.ag-center-cols-container .ag-row')
|
||||
.eq(multiKey ? 3 : 1)
|
||||
.within(() => {
|
||||
cy.get('[col-id="marketName"]')
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
|
||||
cy.get('[col-id="openVolume"]').should('not.be.empty');
|
||||
|
||||
// includes average entry price, mark price, realised PNL & leverage
|
||||
cy.getByTestId('flash-cell').should('not.be.empty');
|
||||
|
||||
if (!multiKey) {
|
||||
cy.get('[col-id="currentLeverage"]').should('contain.text', '2,767.3');
|
||||
cy.get('[col-id="marginAccountBalance"]') // margin allocated
|
||||
.should('contain.text', '0.01');
|
||||
}
|
||||
|
||||
cy.get('[col-id="unrealisedPNL"]').should('not.be.empty');
|
||||
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
|
||||
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
|
||||
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
|
||||
});
|
||||
|
||||
cy.get('.ag-header-row [col-id="notional"]')
|
||||
.should('contain.text', 'Notional')
|
||||
.realHover();
|
||||
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
|
||||
|
||||
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
|
||||
}
|
||||
|
||||
function assertPNLColor(
|
||||
pnlSelector: string,
|
||||
positiveClass: string,
|
||||
negativeClass: string
|
||||
) {
|
||||
cy.get(pnlSelector).each(($el) => {
|
||||
const value = parseFloat($el.text());
|
||||
|
||||
if (value > 0) {
|
||||
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
|
||||
} else if (value < 0) {
|
||||
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
|
||||
} else if (value == 0) {
|
||||
cy.wrap($el)
|
||||
.invoke('attr', 'class')
|
||||
.should('not.contain', negativeClass, positiveClass);
|
||||
} else {
|
||||
throw new Error('Unexpected value');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function visitAndClickPositions() {
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId(positions).click();
|
||||
}
|
||||
@@ -94,11 +94,7 @@ const MainGrid = memo(
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-bottom">
|
||||
<Tab
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
menu={<TradingViews.positions.menu />}
|
||||
>
|
||||
<Tab id="positions" name={t('Positions')}>
|
||||
<TradingViews.positions.component />
|
||||
</Tab>
|
||||
<Tab
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { OrderContainerProps } from '../../components/orders-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||
import { AccountsMenu } from '../../components/accounts-menu';
|
||||
import { PositionsMenu } from '../../components/positions-menu';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
@@ -58,11 +57,7 @@ export const TradingViews = {
|
||||
label: 'Trades',
|
||||
component: requiresMarket(TradesContainer),
|
||||
},
|
||||
positions: {
|
||||
label: 'Positions',
|
||||
component: PositionsContainer,
|
||||
menu: PositionsMenu,
|
||||
},
|
||||
positions: { label: 'Positions', component: PositionsContainer },
|
||||
activeOrders: {
|
||||
label: 'Active',
|
||||
component: (props: OrderContainerProps) => (
|
||||
|
||||
@@ -89,15 +89,17 @@ const MarketData = ({
|
||||
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
|
||||
: '0.00';
|
||||
|
||||
const productType = market.tradableInstrument.instrument.product.__typename;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
|
||||
{market.tradableInstrument.instrument.code}{' '}
|
||||
{allProducts && productType && (
|
||||
<MarketProductPill productType={productType} />
|
||||
{allProducts && (
|
||||
<MarketProductPill
|
||||
productType={
|
||||
market.tradableInstrument.instrument.product.__typename
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</h3>
|
||||
{mode && (
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
|
||||
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
|
||||
import type { StateCreator } from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
@@ -14,7 +13,6 @@ export const PositionsContainer = ({ allKeys }: { allKeys?: boolean }) => {
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
|
||||
|
||||
const showClosed = usePositionsStore((store) => store.showClosedMarkets);
|
||||
const gridStore = usePositionsStore((store) => store.gridStore);
|
||||
const updateGridStore = usePositionsStore((store) => store.updateGridStore);
|
||||
const gridStoreCallbacks = useDataGridEvents(gridStore, updateGridStore);
|
||||
@@ -42,35 +40,12 @@ export const PositionsContainer = ({ allKeys }: { allKeys?: boolean }) => {
|
||||
onMarketClick={onMarketClick}
|
||||
isReadOnly={isReadOnly}
|
||||
gridProps={gridStoreCallbacks}
|
||||
showClosed={showClosed}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type PositionsStoreSlice = {
|
||||
showClosedMarkets: boolean;
|
||||
toggleClosedMarkets: () => void;
|
||||
};
|
||||
|
||||
const createPositionStoreSlice: StateCreator<PositionsStoreSlice> = (set) => ({
|
||||
showClosedMarkets: false,
|
||||
toggleClosedMarkets: () => {
|
||||
set((curr) => {
|
||||
return {
|
||||
showClosedMarkets: !curr.showClosedMarkets,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const usePositionsStore = create<PositionsStoreSlice & DataGridSlice>()(
|
||||
persist(
|
||||
(...args) => ({
|
||||
...createPositionStoreSlice(...args),
|
||||
...createDataGridSlice(...args),
|
||||
}),
|
||||
{
|
||||
name: 'vega_positions_store',
|
||||
}
|
||||
)
|
||||
const usePositionsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_positions_store',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './positions-menu';
|
||||
@@ -1,18 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { usePositionsStore } from '../positions-container';
|
||||
|
||||
export const PositionsMenu = () => {
|
||||
const showClosed = usePositionsStore((store) => store.showClosedMarkets);
|
||||
const toggle = usePositionsStore((store) => store.toggleClosedMarkets);
|
||||
return (
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
data-testid="open-transfer"
|
||||
onClick={toggle}
|
||||
>
|
||||
{showClosed ? t('Hide closed markets') : t('Show closed markets')}
|
||||
</TradingButton>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +1,10 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
import { Head, Html, Main, NextScript } from 'next/document';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
<>
|
||||
<Head>
|
||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charSet="utf-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Vega Protocol - VEGA Console" />
|
||||
<meta name="og:type" content="website" />
|
||||
<meta name="og:url" content="https://console.vega.xyz/" />
|
||||
<meta name="og:title" content="Vega Protocol - Console" />
|
||||
<meta name="og:site_name" content="Vega Protocol - Console" />
|
||||
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
|
||||
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<meta name="twitter:title" content="Vega Protocol - Console" />
|
||||
<meta name="twitter:description" content="Vega Protocol - Console" />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<meta name="twitter:image:alt" content="VEGA logo" />
|
||||
<meta name="twitter:site" content="@vegaprotocol" />
|
||||
|
||||
<meta name="description" content="Vega Protocol - Console" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
content="https://static.vega.xyz/favicon.ico"
|
||||
@@ -45,8 +15,6 @@ export default function Document() {
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/preloader.css" media="all" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
@@ -54,10 +22,12 @@ export default function Document() {
|
||||
/>
|
||||
<script src="/theme-setter.js" type="text/javascript" async />
|
||||
</Head>
|
||||
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
<Html>
|
||||
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Head from 'next/head';
|
||||
import { ClientRouter } from './client-router';
|
||||
|
||||
/**
|
||||
@@ -6,5 +7,60 @@ import { ClientRouter } from './client-router';
|
||||
* have to serve a static site via next export
|
||||
*/
|
||||
export default function Index() {
|
||||
return <ClientRouter />;
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charSet="utf-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Vega Protocol - VEGA Console" />
|
||||
<meta name="og:type" content="website" />
|
||||
<meta name="og:url" content="https://console.vega.xyz/" />
|
||||
<meta name="og:title" content="Vega Protocol - Console" />
|
||||
<meta name="og:site_name" content="Vega Protocol - Console" />
|
||||
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
|
||||
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<meta name="twitter:title" content="Vega Protocol - Console" />
|
||||
<meta name="twitter:description" content="Vega Protocol - Console" />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<meta name="twitter:image:alt" content="VEGA logo" />
|
||||
<meta name="twitter:site" content="@vegaprotocol" />
|
||||
|
||||
<meta name="description" content="Vega Protocol - Console" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
content="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/preloader.css" media="all" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://static.vega.xyz/favicon.ico"
|
||||
/>
|
||||
<script src="/theme-setter.js" type="text/javascript" async />
|
||||
</Head>
|
||||
<ClientRouter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,27 +2,28 @@ import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { Pill } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ProductTypeShortName,
|
||||
type Market,
|
||||
type ProductType,
|
||||
ProductTypeMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/types';
|
||||
|
||||
const productTypeMap = {
|
||||
Future: 'Futr',
|
||||
FutureProduct: 'Futr',
|
||||
Spot: 'Spot',
|
||||
SpotProduct: 'Spot',
|
||||
Perpetual: 'Perp',
|
||||
PerpetualProduct: 'Perp',
|
||||
} as const;
|
||||
export type ProductType = keyof typeof productTypeMap | undefined;
|
||||
|
||||
export const MarketProductPill = ({
|
||||
productType,
|
||||
}: {
|
||||
productType: ProductType;
|
||||
productType?: ProductType;
|
||||
}) => {
|
||||
return (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
return productType ? (
|
||||
<Pill size="xxs" className="uppercase ml-0.5" title={productType}>
|
||||
{productTypeMap[productType] || productType}
|
||||
</Pill>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
interface MarketNameCellProps {
|
||||
@@ -65,7 +66,7 @@ export const MarketNameCell = ({
|
||||
<span data-testid="market-code" data-market-id={id}>
|
||||
{value}
|
||||
</span>
|
||||
{productType && <MarketProductPill productType={productType} />}
|
||||
<MarketProductPill productType={productType} />
|
||||
</>
|
||||
);
|
||||
return onMarketClick && id ? (
|
||||
|
||||
@@ -107,7 +107,6 @@ export const NetworkParams = {
|
||||
market_liquidity_targetstake_triggering_ratio:
|
||||
'market_liquidity_targetstake_triggering_ratio',
|
||||
transfer_fee_factor: 'transfer_fee_factor',
|
||||
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
|
||||
} as const;
|
||||
|
||||
type Params = typeof NetworkParams;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEstimatePositionQuery } from './__generated__/Positions';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { formatRange } from '@vegaprotocol/utils';
|
||||
|
||||
export const LiquidationPrice = ({
|
||||
marketId,
|
||||
openVolume,
|
||||
collateralAvailable,
|
||||
marketDecimalPlaces,
|
||||
decimalPlaces,
|
||||
formatDecimals,
|
||||
}: {
|
||||
marketId: string;
|
||||
openVolume: string;
|
||||
collateralAvailable: string;
|
||||
marketDecimalPlaces: number;
|
||||
decimalPlaces: number;
|
||||
formatDecimals: number;
|
||||
}) => {
|
||||
const { data: currentData, previousData } = useEstimatePositionQuery({
|
||||
variables: {
|
||||
@@ -23,47 +23,38 @@ export const LiquidationPrice = ({
|
||||
fetchPolicy: 'no-cache',
|
||||
skip: !openVolume || openVolume === '0',
|
||||
});
|
||||
|
||||
const data = currentData || previousData;
|
||||
let value = '-';
|
||||
|
||||
if (!data?.estimatePosition?.liquidation) {
|
||||
return <span>-</span>;
|
||||
if (data) {
|
||||
const bestCase =
|
||||
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
|
||||
/\..*/,
|
||||
''
|
||||
);
|
||||
const worstCase =
|
||||
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
|
||||
/\..*/,
|
||||
''
|
||||
);
|
||||
value =
|
||||
bestCase && worstCase && BigInt(bestCase) < BigInt(worstCase)
|
||||
? formatRange(
|
||||
bestCase,
|
||||
worstCase,
|
||||
decimalPlaces,
|
||||
undefined,
|
||||
formatDecimals,
|
||||
value
|
||||
)
|
||||
: formatRange(
|
||||
worstCase,
|
||||
bestCase,
|
||||
decimalPlaces,
|
||||
undefined,
|
||||
formatDecimals,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
let bestCase = '-';
|
||||
let worstCase = '-';
|
||||
|
||||
bestCase =
|
||||
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
|
||||
/\..*/,
|
||||
''
|
||||
);
|
||||
worstCase =
|
||||
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
|
||||
/\..*/,
|
||||
''
|
||||
);
|
||||
worstCase = addDecimalsFormatNumber(worstCase, marketDecimalPlaces);
|
||||
bestCase = addDecimalsFormatNumber(bestCase, marketDecimalPlaces);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t('Worst case')}</th>
|
||||
<td className="text-right font-mono pl-2">{worstCase}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('Best case')}</th>
|
||||
<td className="text-right font-mono pl-2">{bestCase}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
>
|
||||
<span data-testid="liquidation-price">{worstCase}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
return <span data-testid="liquidation-price">{value}</span>;
|
||||
};
|
||||
|
||||
@@ -2,12 +2,7 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import type { MarketWithData } from '@vegaprotocol/markets';
|
||||
import type { PositionFieldsFragment } from './__generated__/Positions';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import {
|
||||
getMetrics,
|
||||
preparePositions,
|
||||
rejoinPositionData,
|
||||
} from './positions-data-providers';
|
||||
import { getMetrics, rejoinPositionData } from './positions-data-providers';
|
||||
import { PositionStatus } from '@vegaprotocol/types';
|
||||
|
||||
const accounts = [
|
||||
@@ -185,12 +180,12 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
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].decimals).toEqual(5);
|
||||
expect(metrics[0].markPrice).toEqual('9431775');
|
||||
expect(metrics[0].marketId).toEqual(
|
||||
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
|
||||
);
|
||||
expect(metrics[0].marketCode).toEqual('AAVEDAI.MF21');
|
||||
expect(metrics[0].marketName).toEqual('AAVEDAI.MF21');
|
||||
expect(metrics[0].marketTradingMode).toEqual(
|
||||
'TRADING_MODE_MONITORING_AUCTION'
|
||||
);
|
||||
@@ -210,12 +205,12 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
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].decimals).toEqual(5);
|
||||
expect(metrics[1].markPrice).toEqual('869762');
|
||||
expect(metrics[1].marketId).toEqual(
|
||||
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
|
||||
);
|
||||
expect(metrics[1].marketCode).toEqual('UNIDAI.MF21');
|
||||
expect(metrics[1].marketName).toEqual('UNIDAI.MF21');
|
||||
expect(metrics[1].marketTradingMode).toEqual('TRADING_MODE_CONTINUOUS');
|
||||
expect(metrics[1].notional).toEqual('86976200');
|
||||
expect(metrics[1].openVolume).toEqual('-100');
|
||||
@@ -228,29 +223,4 @@ describe('getMetrics && rejoinPositionData', () => {
|
||||
);
|
||||
expect(metrics[1].status).toEqual(positions[1].positionStatus);
|
||||
});
|
||||
|
||||
it('sorts and filters positions', () => {
|
||||
const createPosition = (override?: Partial<Position>) =>
|
||||
({
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
marketCode: 'a',
|
||||
...override,
|
||||
} as Position);
|
||||
|
||||
const data = [
|
||||
createPosition(),
|
||||
createPosition({
|
||||
marketCode: 'c',
|
||||
marketState: Schema.MarketState.STATE_CANCELLED,
|
||||
}),
|
||||
createPosition({ marketCode: 'd' }),
|
||||
createPosition({ marketCode: 'b' }),
|
||||
];
|
||||
|
||||
const withoutClosed = preparePositions(data, false);
|
||||
expect(withoutClosed.map((p) => p.marketCode)).toEqual(['a', 'b', 'd']);
|
||||
|
||||
const withClosed = preparePositions(data, true);
|
||||
expect(withClosed.map((p) => p.marketCode)).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,22 +26,21 @@ import {
|
||||
PositionsDocument,
|
||||
PositionsSubscriptionDocument,
|
||||
} from './__generated__/Positions';
|
||||
import type { PositionStatus, ProductType } from '@vegaprotocol/types';
|
||||
import type { PositionStatus } from '@vegaprotocol/types';
|
||||
|
||||
export interface Position {
|
||||
assetId: string;
|
||||
assetSymbol: string;
|
||||
averageEntryPrice: string;
|
||||
currentLeverage: number | undefined;
|
||||
assetDecimals: number;
|
||||
decimals: number;
|
||||
quantum: string;
|
||||
lossSocializationAmount: string;
|
||||
marginAccountBalance: string;
|
||||
marketDecimalPlaces: number;
|
||||
marketId: string;
|
||||
marketCode: string;
|
||||
marketName: string;
|
||||
marketTradingMode: Schema.MarketTradingMode;
|
||||
marketState: Schema.MarketState;
|
||||
markPrice: string | undefined;
|
||||
notional: string | undefined;
|
||||
openVolume: string;
|
||||
@@ -52,7 +51,7 @@ export interface Position {
|
||||
totalBalance: string;
|
||||
unrealisedPNL: string;
|
||||
updatedAt: string | null;
|
||||
productType: ProductType;
|
||||
productType?: string;
|
||||
}
|
||||
|
||||
export const getMetrics = (
|
||||
@@ -72,10 +71,15 @@ export const getMetrics = (
|
||||
const marginAccount = accounts?.find((account) => {
|
||||
return account.market?.id === market?.id;
|
||||
});
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
const {
|
||||
decimals,
|
||||
id: assetId,
|
||||
symbol: assetSymbol,
|
||||
quantum,
|
||||
} = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
const generalAccount = accounts?.find(
|
||||
(account) =>
|
||||
account.asset.id === asset.id &&
|
||||
account.asset.id === assetId &&
|
||||
account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
|
||||
@@ -85,11 +89,11 @@ export const getMetrics = (
|
||||
|
||||
const marginAccountBalance = toBigNum(
|
||||
marginAccount?.balance ?? 0,
|
||||
asset.decimals
|
||||
decimals
|
||||
);
|
||||
const generalAccountBalance = toBigNum(
|
||||
generalAccount?.balance ?? 0,
|
||||
asset.decimals
|
||||
decimals
|
||||
);
|
||||
|
||||
const markPrice = marketData
|
||||
@@ -108,19 +112,18 @@ export const getMetrics = (
|
||||
: notional.dividedBy(totalBalance)
|
||||
: undefined;
|
||||
metrics.push({
|
||||
assetId: asset.id,
|
||||
assetSymbol: asset.symbol,
|
||||
assetId,
|
||||
assetSymbol,
|
||||
averageEntryPrice: position.averageEntryPrice,
|
||||
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
|
||||
assetDecimals: asset.decimals,
|
||||
quantum: asset.quantum,
|
||||
decimals,
|
||||
quantum,
|
||||
lossSocializationAmount: position.lossSocializationAmount || '0',
|
||||
marginAccountBalance: marginAccount?.balance ?? '0',
|
||||
marketDecimalPlaces,
|
||||
marketId: market.id,
|
||||
marketCode: market.tradableInstrument.instrument.code,
|
||||
marketName: market.tradableInstrument.instrument.code,
|
||||
marketTradingMode: market.tradingMode,
|
||||
marketState: market.state,
|
||||
markPrice: marketData ? marketData.markPrice : undefined,
|
||||
notional: notional
|
||||
? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
|
||||
@@ -130,11 +133,10 @@ export const getMetrics = (
|
||||
positionDecimalPlaces,
|
||||
realisedPNL: position.realisedPNL,
|
||||
status: position.positionStatus,
|
||||
totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(),
|
||||
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
|
||||
unrealisedPNL: position.unrealisedPNL,
|
||||
updatedAt: position.updatedAt || null,
|
||||
productType: market?.tradableInstrument.instrument.product
|
||||
.__typename as ProductType,
|
||||
productType: market?.tradableInstrument.instrument.product.__typename,
|
||||
});
|
||||
});
|
||||
return metrics;
|
||||
@@ -250,26 +252,6 @@ export const rejoinPositionData = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const preparePositions = (metrics: Position[], showClosed: boolean) => {
|
||||
return sortBy(metrics, 'marketCode').filter((p) => {
|
||||
if (showClosed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketState.STATE_PENDING,
|
||||
Schema.MarketState.STATE_SUSPENDED,
|
||||
].includes(p.marketState)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
export const positionsMarketsProvider = makeDerivedDataProvider<
|
||||
string[],
|
||||
never,
|
||||
@@ -287,7 +269,7 @@ export const positionsMarketsProvider = makeDerivedDataProvider<
|
||||
export const positionsMetricsProvider = makeDerivedDataProvider<
|
||||
Position[],
|
||||
Position[],
|
||||
PositionsQueryVariables & { marketIds: string[]; showClosed: boolean }
|
||||
PositionsQueryVariables & { marketIds: string[] }
|
||||
>(
|
||||
[
|
||||
(callback, client, variables) =>
|
||||
@@ -303,10 +285,10 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
|
||||
marketIds: variables.marketIds,
|
||||
}),
|
||||
],
|
||||
([positions, accounts, marketsData], variables) => {
|
||||
([positions, accounts, marketsData]) => {
|
||||
const positionsData = rejoinPositionData(positions, marketsData);
|
||||
const metrics = getMetrics(positionsData, accounts as Account[] | null);
|
||||
return preparePositions(metrics, variables.showClosed);
|
||||
return sortBy(metrics, 'marketName');
|
||||
},
|
||||
(data, delta, previousData) =>
|
||||
data.filter((row) => {
|
||||
|
||||
@@ -15,8 +15,7 @@ interface PositionsManagerProps {
|
||||
partyIds: string[];
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
isReadOnly: boolean;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
showClosed?: boolean;
|
||||
gridProps: ReturnType<typeof useDataGridEvents>;
|
||||
}
|
||||
|
||||
export const PositionsManager = ({
|
||||
@@ -24,7 +23,6 @@ export const PositionsManager = ({
|
||||
onMarketClick,
|
||||
isReadOnly,
|
||||
gridProps,
|
||||
showClosed = false,
|
||||
}: PositionsManagerProps) => {
|
||||
const { pubKeys, pubKey } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
@@ -62,7 +60,7 @@ export const PositionsManager = ({
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: positionsMetricsProvider,
|
||||
variables: { partyIds, marketIds: marketIds || [], showClosed },
|
||||
variables: { partyIds, marketIds: marketIds || [] },
|
||||
skip: !marketIds,
|
||||
});
|
||||
|
||||
@@ -70,7 +68,7 @@ export const PositionsManager = ({
|
||||
<PositionsTable
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys}
|
||||
rowData={data}
|
||||
rowData={error ? [] : data}
|
||||
onMarketClick={onMarketClick}
|
||||
onClose={onClose}
|
||||
isReadOnly={isReadOnly}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { RenderResult } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { PositionsTable, OpenVolumeCell, PNLCell } from './positions-table';
|
||||
import PositionsTable, { OpenVolumeCell, PNLCell } from './positions-table';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { PositionStatus } from '@vegaprotocol/types';
|
||||
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
@@ -19,15 +20,14 @@ const singleRow: Position = {
|
||||
assetSymbol: 'BTC',
|
||||
averageEntryPrice: '133',
|
||||
currentLeverage: 1.1,
|
||||
assetDecimals: 2, // this is settlementAsset.decimals
|
||||
decimals: 2, // this is settlementAsset.decimals
|
||||
quantum: '0.1',
|
||||
lossSocializationAmount: '0',
|
||||
marginAccountBalance: '12345600',
|
||||
marketDecimalPlaces: 1,
|
||||
marketId: 'string',
|
||||
marketCode: 'ETHBTC.QM21',
|
||||
marketName: 'ETH/BTC (31 july 2022)',
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
markPrice: '123',
|
||||
notional: '12300',
|
||||
openVolume: '100',
|
||||
@@ -40,13 +40,9 @@ const singleRow: Position = {
|
||||
productType: 'Future',
|
||||
};
|
||||
|
||||
describe('Positions', () => {
|
||||
const renderComponent = async (rowData: Position) => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={[rowData]} isReadOnly={false} />);
|
||||
});
|
||||
};
|
||||
const singleRowData = [singleRow];
|
||||
|
||||
describe('Positions', () => {
|
||||
it('should render successfully', async () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(
|
||||
@@ -57,132 +53,158 @@ describe('Positions', () => {
|
||||
});
|
||||
|
||||
it('render correct columns', async () => {
|
||||
const expectedHeaders = [
|
||||
'Market',
|
||||
'Size / Notional',
|
||||
'Entry / Mark',
|
||||
'Margin',
|
||||
'Liquidation',
|
||||
'Realised PNL',
|
||||
'Unrealised PNL',
|
||||
];
|
||||
|
||||
await renderComponent(singleRow);
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
|
||||
});
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers).toHaveLength(11);
|
||||
expect(
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual(expectedHeaders);
|
||||
).toEqual([
|
||||
'Market',
|
||||
'Notional',
|
||||
'Open volume',
|
||||
'Mark price',
|
||||
'Liquidation price',
|
||||
'Asset',
|
||||
'Entry price',
|
||||
'Leverage',
|
||||
'Margin',
|
||||
'Realised PNL',
|
||||
'Unrealised PNL',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders market code', async () => {
|
||||
await renderComponent(singleRow);
|
||||
expect(screen.getByText(singleRow.marketCode)).toBeTruthy();
|
||||
it('renders market name', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
|
||||
expect(screen.getByText('Futr')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Does not fail if the market name does not match the split pattern', async () => {
|
||||
const breakingMarketName = 'OP/USD AUG-SEP22 - Incentive';
|
||||
await renderComponent({ ...singleRow, marketCode: breakingMarketName });
|
||||
const row = [
|
||||
Object.assign({}, singleRow, { marketName: breakingMarketName }),
|
||||
];
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={row} isReadOnly={false} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText(breakingMarketName)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('displays size / notional correctly for long position', async () => {
|
||||
await renderComponent(singleRow);
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[1];
|
||||
it('add color and sign to amount, displays positive notional value', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<PositionsTable rowData={singleRowData} isReadOnly={false} />
|
||||
);
|
||||
});
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
|
||||
expect(cell).toHaveClass('text-market-green-600');
|
||||
expect(cell).not.toHaveClass('text-market-red');
|
||||
|
||||
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
'+100'
|
||||
);
|
||||
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'1,230.0'
|
||||
);
|
||||
expect(cells[2].classList.contains('text-market-green-600')).toBeTruthy();
|
||||
expect(cells[2].classList.contains('text-market-red')).toBeFalsy();
|
||||
expect(cells[2].textContent).toEqual('+100');
|
||||
expect(cells[1].textContent).toEqual('1,230.0');
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '-100' }]}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[2].classList.contains('text-market-green-600')).toBeFalsy();
|
||||
expect(cells[2].classList.contains('text-market-red')).toBeTruthy();
|
||||
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
|
||||
expect(cells[1].textContent).toEqual('1,230.0');
|
||||
});
|
||||
|
||||
it('displays size / notional correctly for short position', async () => {
|
||||
await renderComponent({ ...singleRow, openVolume: '-100' });
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[1];
|
||||
|
||||
expect(cell).not.toHaveClass('text-market-green-600');
|
||||
expect(cell).toHaveClass('text-market-red');
|
||||
|
||||
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
'-100'
|
||||
);
|
||||
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'1,230.0'
|
||||
);
|
||||
});
|
||||
|
||||
it('displays entry / mark price', async () => {
|
||||
await renderComponent(singleRow);
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = within(cells[2]);
|
||||
expect(cell.getByTestId('stack-cell-primary')).toHaveTextContent('13.3');
|
||||
expect(cell.getByTestId('stack-cell-secondary')).toHaveTextContent('12.3');
|
||||
});
|
||||
|
||||
it('doesnt render entry / mark if market is in opening auction', async () => {
|
||||
await renderComponent({
|
||||
...singleRow,
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
it('displays mark price', async () => {
|
||||
let result: RenderResult;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<PositionsTable rowData={singleRowData} isReadOnly={false} />
|
||||
);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[2].textContent).toEqual('-');
|
||||
let cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3].textContent).toEqual('12.3');
|
||||
|
||||
await act(async () => {
|
||||
result.rerender(
|
||||
<PositionsTable
|
||||
rowData={[
|
||||
{
|
||||
...singleRow,
|
||||
marketTradingMode:
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
},
|
||||
]}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[3].textContent).toEqual('-');
|
||||
});
|
||||
|
||||
it('displays liquidation price', async () => {
|
||||
await renderComponent(singleRow);
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[4].textContent).toEqual('liquidation price');
|
||||
});
|
||||
|
||||
it('displays margin and leverage', async () => {
|
||||
await renderComponent(singleRow);
|
||||
it('displays leverage', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[7].textContent).toEqual('1.1');
|
||||
});
|
||||
|
||||
// margin
|
||||
expect(
|
||||
within(cells[3]).getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('123,456.00');
|
||||
|
||||
// leverage
|
||||
expect(
|
||||
within(cells[3]).getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('1.1');
|
||||
it('displays allocated margin', async () => {
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[8];
|
||||
expect(cell.textContent).toEqual('123,456.00');
|
||||
});
|
||||
|
||||
it('displays realised and unrealised PNL', async () => {
|
||||
// pnl cells should be rendered with asset dps
|
||||
const expectedRealised = addDecimalsFormatNumber(
|
||||
singleRow.realisedPNL,
|
||||
singleRow.assetDecimals
|
||||
singleRow.decimals
|
||||
);
|
||||
const expectedUnrealised = addDecimalsFormatNumber(
|
||||
singleRow.unrealisedPNL,
|
||||
singleRow.assetDecimals
|
||||
singleRow.decimals
|
||||
);
|
||||
|
||||
await renderComponent(singleRow);
|
||||
await act(async () => {
|
||||
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[5]).toHaveTextContent(expectedRealised);
|
||||
expect(cells[6]).toHaveTextContent(expectedUnrealised);
|
||||
expect(cells[9].textContent).toEqual(expectedRealised);
|
||||
expect(cells[10].textContent).toEqual(expectedUnrealised);
|
||||
});
|
||||
|
||||
it('displays close button', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={[singleRow]}
|
||||
pubKey={singleRow.partyId}
|
||||
rowData={singleRowData}
|
||||
pubKey={singleRowData[0].partyId}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
@@ -190,15 +212,24 @@ describe('Positions', () => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('close-position')).toBeInTheDocument();
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[11].textContent).toEqual('');
|
||||
});
|
||||
|
||||
it('do not display close button if openVolume is zero', async () => {
|
||||
await renderComponent({ ...singleRow, openVolume: '0' });
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Close' })
|
||||
).not.toBeInTheDocument();
|
||||
await act(async () => {
|
||||
render(
|
||||
<PositionsTable
|
||||
rowData={[{ ...singleRow, openVolume: '0' }]}
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
expect(cells[11].textContent).toEqual('');
|
||||
});
|
||||
|
||||
describe('PNLCell', () => {
|
||||
@@ -218,27 +249,40 @@ describe('Positions', () => {
|
||||
lossSocialisationAmount: '0',
|
||||
},
|
||||
valueFormatted: '100',
|
||||
} as ICellRendererParams;
|
||||
render(<PNLCell {...props} />);
|
||||
expect(
|
||||
screen.getByText(props.valueFormatted as string)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders value with warning icon if loss socialisation occurred', () => {
|
||||
it('renders value with warning tooltip if loss socialisation occurred', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
lossSocializationAmount: '500',
|
||||
assetDecimals: 2,
|
||||
decimals: 2,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
};
|
||||
render(<PNLCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
'Lifetime loss socialisation deductions: 5.00'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(tooltip).getByText(
|
||||
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,10 +290,9 @@ describe('Positions', () => {
|
||||
const props = {
|
||||
data: undefined,
|
||||
valueFormatted: '100',
|
||||
} as ICellRendererParams;
|
||||
|
||||
};
|
||||
it('renders a dash if no data', () => {
|
||||
render(<OpenVolumeCell {...props} />);
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -263,21 +306,36 @@ describe('Positions', () => {
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if orders were closed', () => {
|
||||
it('renders status with warning tooltip if orders were closed', async () => {
|
||||
const props = {
|
||||
data: {
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
} as ICellRendererParams;
|
||||
render(<OpenVolumeCell {...props} />);
|
||||
const content = screen.getByText(props.valueFormatted as string);
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status with warning tooltip if position was closed out', async () => {
|
||||
@@ -287,71 +345,24 @@ describe('Positions', () => {
|
||||
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
|
||||
},
|
||||
valueFormatted: '100',
|
||||
} as ICellRendererParams;
|
||||
render(<OpenVolumeCell {...props} />);
|
||||
const content = screen.getByText(props.valueFormatted as string);
|
||||
};
|
||||
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
|
||||
const content = screen.getByText(props.valueFormatted);
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(screen.getByTestId(/icon-/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('position status from size column', () => {
|
||||
it('does not show if position status is normal', async () => {
|
||||
await renderComponent({
|
||||
...singleRow,
|
||||
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[1];
|
||||
await userEvent.hover(cell);
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
status: PositionStatus.POSITION_STATUS_CLOSED_OUT,
|
||||
text: 'Your position was closed.',
|
||||
},
|
||||
{
|
||||
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
|
||||
text: 'Your open orders were cancelled.',
|
||||
},
|
||||
{
|
||||
status: PositionStatus.POSITION_STATUS_DISTRESSED,
|
||||
text: 'Your position is distressed.',
|
||||
},
|
||||
])('renders content for $status', async (data) => {
|
||||
await renderComponent({
|
||||
...singleRow,
|
||||
status: data.status,
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[1];
|
||||
await userEvent.hover(cell);
|
||||
const tooltip = within(await screen.findByRole('tooltip'));
|
||||
expect(tooltip.getByText(data.text)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loss socialization from realised pnl column', () => {
|
||||
it('renders', async () => {
|
||||
await renderComponent({
|
||||
...singleRow,
|
||||
lossSocializationAmount: '500',
|
||||
assetDecimals: 2,
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const cell = cells[5];
|
||||
|
||||
await userEvent.hover(cell);
|
||||
const tooltip = within(await screen.findByRole('tooltip'));
|
||||
expect(tooltip.getByText('Realised PNL: 1.23')).toBeInTheDocument();
|
||||
expect(screen.getByRole('img')).toBeInTheDocument();
|
||||
await userEvent.hover(content);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(
|
||||
tooltip.getByText('Lifetime loss socialisation deductions: 5.00')
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
`Status: ${PositionStatusMapping[props.data.status]}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
tooltip.getByText(
|
||||
`You received less BTC in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`
|
||||
// using within as radix renders tooltip content twice
|
||||
within(tooltip).getByText(
|
||||
'You did not have enough BTC collateral to meet the maintenance margin requirements for your position, so it was closed by the network.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import type { ColDef, ITooltipParams } from 'ag-grid-community';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import type {
|
||||
VegaValueFormatterParams,
|
||||
VegaValueGetterParams,
|
||||
TypedDataAgGrid,
|
||||
VegaICellRendererParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { COL_DEFS } from '@vegaprotocol/datagrid';
|
||||
import { ProgressBarCell } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
AgGridLazy as AgGrid,
|
||||
COL_DEFS,
|
||||
PriceFlashCell,
|
||||
signedNumberCssClass,
|
||||
signedNumberCssClassRules,
|
||||
MarketNameCell,
|
||||
ProgressBarCell,
|
||||
MarketProductPill,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
ButtonLink,
|
||||
Tooltip,
|
||||
TooltipCellComponent,
|
||||
ExternalLink,
|
||||
VegaIcon,
|
||||
Icon,
|
||||
VegaIconNames,
|
||||
VegaIcon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
volumePrefix,
|
||||
toBigNum,
|
||||
formatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import {
|
||||
MarketTradingMode,
|
||||
PositionStatus,
|
||||
PositionStatusMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { PositionActionsDropdown } from './position-actions-dropdown';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { LiquidationPrice } from './liquidation-price';
|
||||
import { StackedCell } from './stacked-cell';
|
||||
|
||||
interface Props extends TypedDataAgGrid<Position> {
|
||||
onClose?: (data: Position) => void;
|
||||
@@ -48,25 +48,44 @@ interface Props extends TypedDataAgGrid<Position> {
|
||||
style?: CSSProperties;
|
||||
isReadOnly: boolean;
|
||||
multipleKeys?: boolean;
|
||||
pubKeys?: Array<{ name: string; publicKey: string }> | null;
|
||||
pubKey?: string | null;
|
||||
pubKeys?: VegaWalletContextShape['pubKeys'];
|
||||
pubKey?: VegaWalletContextShape['pubKey'];
|
||||
}
|
||||
|
||||
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;
|
||||
return valueFormatted && notional ? (
|
||||
<div className="leading-tight font-mono">
|
||||
<div
|
||||
className={classNames('text-right', signedNumberCssClass(openVolume))}
|
||||
>
|
||||
{volumePrefix(
|
||||
addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{addDecimalsFormatNumber(notional, marketDecimalPlaces)}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
AmountCell.displayName = 'AmountCell';
|
||||
|
||||
export const getRowId = ({ data }: { data: Position }) =>
|
||||
`${data.partyId}-${data.marketId}`;
|
||||
|
||||
const realisedPNLValueGetter = ({ data }: { data: Position }) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.realisedPNL, data.assetDecimals).toNumber();
|
||||
};
|
||||
|
||||
const unrealisedPNLValueGetter = ({ data }: { data: Position }) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.unrealisedPNL, data.assetDecimals).toNumber();
|
||||
};
|
||||
|
||||
const defaultColDef = {
|
||||
sortable: true,
|
||||
filter: true,
|
||||
@@ -84,6 +103,7 @@ export const PositionsTable = ({
|
||||
pubKey,
|
||||
...props
|
||||
}: Props) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AgGrid
|
||||
overlayNoRowsTemplate={t('No positions')}
|
||||
@@ -91,11 +111,11 @@ export const PositionsTable = ({
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={defaultColDef}
|
||||
components={{
|
||||
AmountCell,
|
||||
PriceFlashCell,
|
||||
ProgressBarCell,
|
||||
MarketNameCell,
|
||||
}}
|
||||
rowHeight={45}
|
||||
columnDefs={useMemo<ColDef[]>(() => {
|
||||
const columnDefs: (ColDef | null)[] = [
|
||||
multipleKeys
|
||||
@@ -112,37 +132,41 @@ export const PositionsTable = ({
|
||||
: null,
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'marketCode',
|
||||
onCellClicked: ({ data }) => {
|
||||
if (!onMarketClick) return;
|
||||
onMarketClick(data.marketId);
|
||||
field: 'marketName',
|
||||
cellRenderer: 'MarketNameCell',
|
||||
cellRendererParams: { idPath: 'marketId', onMarketClick },
|
||||
},
|
||||
{
|
||||
headerName: t('Notional'),
|
||||
headerTooltip: t('Mark price x open volume.'),
|
||||
field: 'notional',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return !data?.notional
|
||||
? undefined
|
||||
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
|
||||
},
|
||||
cellRenderer: ({
|
||||
value,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'marketCode'>) => {
|
||||
if (!data || !value) return '-';
|
||||
return (
|
||||
<StackedCell
|
||||
primary={value}
|
||||
secondary={
|
||||
<>
|
||||
{data?.assetSymbol}
|
||||
<MarketProductPill productType={data.productType} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}: VegaValueFormatterParams<Position, 'notional'>) => {
|
||||
return !data || !data.notional
|
||||
? '-'
|
||||
: addDecimalsFormatNumber(
|
||||
data.notional,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Size / Notional'),
|
||||
headerName: t('Open volume'),
|
||||
field: 'openVolume',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
cellClassRules: signedNumberCssClassRules,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: { data: Position }) => {
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return data?.openVolume === undefined
|
||||
? undefined
|
||||
: toBigNum(
|
||||
@@ -150,205 +174,165 @@ export const PositionsTable = ({
|
||||
data.positionDecimalPlaces
|
||||
).toNumber();
|
||||
},
|
||||
tooltipValueGetter: ({ data }: ITooltipParams<Position>) => {
|
||||
if (
|
||||
!data ||
|
||||
data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return data.status;
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
|
||||
if (!data?.openVolume) return '-';
|
||||
|
||||
const vol = volumePrefix(
|
||||
addDecimalsFormatNumber(
|
||||
data.openVolume,
|
||||
data.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
|
||||
return vol;
|
||||
},
|
||||
tooltipComponent: (args: ITooltipParams<Position>) => {
|
||||
if (!args.data) {
|
||||
return null;
|
||||
}
|
||||
const POSITION_RESOLUTION_LINK =
|
||||
DocsLinks?.POSITION_RESOLUTION ?? '';
|
||||
let primaryTooltip;
|
||||
switch (args.data.status) {
|
||||
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
|
||||
primaryTooltip = t('Your position was closed.');
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
|
||||
primaryTooltip = t('Your open orders were cancelled.');
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_DISTRESSED:
|
||||
primaryTooltip = t('Your position is distressed.');
|
||||
break;
|
||||
}
|
||||
|
||||
let secondaryTooltip;
|
||||
switch (args.data.status) {
|
||||
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
|
||||
secondaryTooltip = t(
|
||||
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
|
||||
args.data.assetSymbol
|
||||
return data?.openVolume === undefined
|
||||
? ''
|
||||
: volumePrefix(
|
||||
addDecimalsFormatNumber(
|
||||
data.openVolume,
|
||||
data.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
|
||||
secondaryTooltip = t(
|
||||
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
|
||||
);
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_DISTRESSED:
|
||||
secondaryTooltip = t(
|
||||
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
secondaryTooltip = t('Maintained by network');
|
||||
}
|
||||
return (
|
||||
<TooltipCellComponent
|
||||
{...args}
|
||||
value={
|
||||
<>
|
||||
<p className="mb-2">{primaryTooltip}</p>
|
||||
<p className="mb-2">{secondaryTooltip}</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'Status: %s',
|
||||
PositionStatusMapping[args.data.status]
|
||||
)}
|
||||
</p>
|
||||
{POSITION_RESOLUTION_LINK && (
|
||||
<ExternalLink href={POSITION_RESOLUTION_LINK}>
|
||||
{t('Read more about position resolution')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
cellRenderer: OpenVolumeCell,
|
||||
},
|
||||
{
|
||||
headerName: t('Entry / Mark'),
|
||||
headerName: t('Mark price'),
|
||||
field: 'markPrice',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'markPrice'>) => {
|
||||
if (
|
||||
!data?.averageEntryPrice ||
|
||||
!data?.markPrice ||
|
||||
!data?.marketDecimalPlaces
|
||||
) {
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
if (
|
||||
data.marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
) {
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
const entry = addDecimalsFormatNumber(
|
||||
data.averageEntryPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
const mark = addDecimalsFormatNumber(
|
||||
data.markPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
return (
|
||||
<StackedCell
|
||||
primary={entry}
|
||||
secondary={
|
||||
<PriceFlashCell
|
||||
value={Number(data.markPrice)}
|
||||
valueFormatted={mark}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
cellRenderer: PriceFlashCell,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return !data ||
|
||||
!data.markPrice ||
|
||||
data.marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
? undefined
|
||||
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Margin'),
|
||||
colId: 'margin',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data.marginAccountBalance,
|
||||
data.assetDecimals
|
||||
).toNumber();
|
||||
},
|
||||
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
|
||||
if (
|
||||
!data ||
|
||||
!data.marginAccountBalance ||
|
||||
!data.marketDecimalPlaces
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const margin = addDecimalsFormatNumberQuantum(
|
||||
data.marginAccountBalance,
|
||||
data.assetDecimals,
|
||||
data.quantum
|
||||
);
|
||||
|
||||
const lev = data?.currentLeverage ? data.currentLeverage : 1;
|
||||
const leverage = formatNumber(Math.max(1, lev), 1);
|
||||
return (
|
||||
<StackedCell primary={margin} secondary={leverage + 'x'} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'liquidationPrice',
|
||||
headerName: 'Liquidation',
|
||||
headerTooltip: t('Worst case liquidation price'),
|
||||
cellClass: 'font-mono text-right',
|
||||
type: 'rightAligned',
|
||||
// Cannot be sortable as data is fetched within the cell
|
||||
sortable: false,
|
||||
filter: false,
|
||||
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
!data.markPrice ||
|
||||
data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
data.markPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Liquidation price'),
|
||||
colId: 'liquidationPrice',
|
||||
type: 'rightAligned',
|
||||
cellClass: 'font-mono text-right',
|
||||
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<LiquidationPrice
|
||||
marketId={data.marketId}
|
||||
openVolume={data.openVolume}
|
||||
collateralAvailable={data.totalBalance}
|
||||
marketDecimalPlaces={data.marketDecimalPlaces}
|
||||
decimalPlaces={data.decimals}
|
||||
formatDecimals={data.marketDecimalPlaces}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Asset'),
|
||||
field: 'assetSymbol',
|
||||
colId: 'asset',
|
||||
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<ButtonLink
|
||||
title={t('View settlement asset details')}
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(
|
||||
data.assetId,
|
||||
e.target as HTMLElement
|
||||
);
|
||||
}}
|
||||
>
|
||||
{data?.assetSymbol}
|
||||
</ButtonLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Entry price'),
|
||||
field: 'averageEntryPrice',
|
||||
type: 'rightAligned',
|
||||
cellRenderer: PriceFlashCell,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return data?.markPrice === undefined || !data
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data.averageEntryPrice,
|
||||
data.marketDecimalPlaces
|
||||
).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<
|
||||
Position,
|
||||
'averageEntryPrice'
|
||||
>): string => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
data.averageEntryPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
},
|
||||
},
|
||||
multipleKeys
|
||||
? null
|
||||
: {
|
||||
headerName: t('Leverage'),
|
||||
field: 'currentLeverage',
|
||||
type: 'rightAligned',
|
||||
filter: 'agNumberColumnFilter',
|
||||
cellRenderer: PriceFlashCell,
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
|
||||
value === undefined ? '' : formatNumber(value.toString(), 1),
|
||||
},
|
||||
multipleKeys
|
||||
? null
|
||||
: {
|
||||
headerName: t('Margin'),
|
||||
field: 'marginAccountBalance',
|
||||
type: 'rightAligned',
|
||||
filter: 'agNumberColumnFilter',
|
||||
cellRenderer: PriceFlashCell,
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data.marginAccountBalance,
|
||||
data.decimals
|
||||
).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<
|
||||
Position,
|
||||
'marginAccountBalance'
|
||||
>): string => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
data.marginAccountBalance,
|
||||
data.decimals
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Realised PNL'),
|
||||
field: 'realisedPNL',
|
||||
@@ -356,71 +340,17 @@ export const PositionsTable = ({
|
||||
cellClassRules: signedNumberCssClassRules,
|
||||
cellClass: 'font-mono text-right',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: realisedPNLValueGetter,
|
||||
// @ts-ignore no type overlap, but the functions are identical
|
||||
tooltipValueGetter: realisedPNLValueGetter,
|
||||
tooltipComponent: (args: ITooltipParams) => {
|
||||
const LOSS_SOCIALIZATION_LINK =
|
||||
DocsLinks?.LOSS_SOCIALIZATION ?? '';
|
||||
|
||||
if (!args.data) {
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
const losses = parseInt(
|
||||
args.data?.lossSocializationAmount ?? '0'
|
||||
);
|
||||
|
||||
if (losses <= 0) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{args.valueFormatted}</>;
|
||||
}
|
||||
|
||||
const lossesFormatted = addDecimalsFormatNumber(
|
||||
args.data.lossSocializationAmount,
|
||||
args.data.assetDecimals
|
||||
);
|
||||
|
||||
return (
|
||||
<TooltipCellComponent
|
||||
{...args}
|
||||
value={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t('Realised PNL: %s', args.value)}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'Lifetime loss socialisation deductions: %s',
|
||||
lossesFormatted
|
||||
)}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
|
||||
args.data.assetSymbol
|
||||
)}
|
||||
</p>
|
||||
{LOSS_SOCIALIZATION_LINK && (
|
||||
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
|
||||
{t('Read more about loss socialisation')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.realisedPNL, data.decimals).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
|
||||
return !data
|
||||
? ''
|
||||
: addDecimalsFormatNumberQuantum(
|
||||
data.realisedPNL,
|
||||
data.assetDecimals,
|
||||
data.quantum
|
||||
);
|
||||
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
|
||||
},
|
||||
headerTooltip: t(
|
||||
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
|
||||
@@ -434,22 +364,21 @@ export const PositionsTable = ({
|
||||
cellClassRules: signedNumberCssClassRules,
|
||||
cellClass: 'font-mono text-right',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: unrealisedPNLValueGetter,
|
||||
// @ts-ignore no type overlap but function can be identical
|
||||
tooltipValueGetter: unrealisedPNLValueGetter,
|
||||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
|
||||
!data
|
||||
? ''
|
||||
: addDecimalsFormatNumberQuantum(
|
||||
data.unrealisedPNL,
|
||||
data.assetDecimals,
|
||||
data.quantum
|
||||
),
|
||||
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
|
||||
headerTooltip: t(
|
||||
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
|
||||
),
|
||||
cellRenderer: PNLCell,
|
||||
},
|
||||
onClose && !isReadOnly
|
||||
? {
|
||||
@@ -473,24 +402,36 @@ export const PositionsTable = ({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
minWidth: 55,
|
||||
maxWidth: 55,
|
||||
minWidth: 75,
|
||||
maxWidth: 75,
|
||||
}
|
||||
: null,
|
||||
];
|
||||
return columnDefs.filter<ColDef>(
|
||||
(colDef: ColDef | null): colDef is ColDef => colDef !== null
|
||||
);
|
||||
}, [isReadOnly, multipleKeys, onClose, onMarketClick, pubKey, pubKeys])}
|
||||
}, [
|
||||
isReadOnly,
|
||||
multipleKeys,
|
||||
onClose,
|
||||
onMarketClick,
|
||||
openAssetDetailsDialog,
|
||||
pubKey,
|
||||
pubKeys,
|
||||
])}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PositionsTable;
|
||||
|
||||
export const PNLCell = ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
|
||||
const LOSS_SOCIALIZATION_LINK = DocsLinks?.LOSS_SOCIALIZATION ?? '';
|
||||
|
||||
if (!data) {
|
||||
return <>-</>;
|
||||
}
|
||||
@@ -501,62 +442,121 @@ export const PNLCell = ({
|
||||
return <>{valueFormatted}</>;
|
||||
}
|
||||
|
||||
return <WarningCell>{valueFormatted}</WarningCell>;
|
||||
const lossesFormatted = addDecimalsFormatNumber(
|
||||
data.lossSocializationAmount,
|
||||
data.decimals
|
||||
);
|
||||
|
||||
return (
|
||||
<WarningCell
|
||||
tooltipContent={
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
`You received less %s in gains that you should have when the market moved in your favour. This occurred because one or more other trader(s) were closed out and did not have enough funds to cover their losses, and the market's insurance pool was empty.`,
|
||||
[data.assetSymbol]
|
||||
)}
|
||||
</p>
|
||||
{LOSS_SOCIALIZATION_LINK && (
|
||||
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
|
||||
{t('Read more about loss socialisation')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{valueFormatted}
|
||||
</WarningCell>
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenVolumeCell = ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'openVolume'>) => {
|
||||
if (!valueFormatted || !data || !data.notional) {
|
||||
if (!data) {
|
||||
return <>-</>;
|
||||
}
|
||||
|
||||
const notional = addDecimalsFormatNumber(
|
||||
data.notional,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
|
||||
|
||||
const cellContent = (
|
||||
<StackedCell primary={valueFormatted} secondary={notional} />
|
||||
);
|
||||
|
||||
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{cellContent}</>;
|
||||
let primaryTooltip;
|
||||
switch (data.status) {
|
||||
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
|
||||
primaryTooltip = t('Your position was closed.');
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
|
||||
primaryTooltip = t('Your open orders were cancelled.');
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_DISTRESSED:
|
||||
primaryTooltip = t('Your position is distressed.');
|
||||
break;
|
||||
}
|
||||
|
||||
let secondaryTooltip;
|
||||
switch (data.status) {
|
||||
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
|
||||
secondaryTooltip = t(
|
||||
`You did not have enough %s collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
|
||||
[data.assetSymbol]
|
||||
);
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
|
||||
secondaryTooltip = t(
|
||||
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
|
||||
);
|
||||
break;
|
||||
case PositionStatus.POSITION_STATUS_DISTRESSED:
|
||||
secondaryTooltip = t(
|
||||
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
secondaryTooltip = t('Maintained by network');
|
||||
}
|
||||
return (
|
||||
<WarningCell
|
||||
showIcon={
|
||||
// not sure why but data.status has become a union of all the enum values
|
||||
// rather than just being the enum itself
|
||||
(data.status as PositionStatus) !==
|
||||
PositionStatus.POSITION_STATUS_UNSPECIFIED
|
||||
showIcon={data.status !== PositionStatus.POSITION_STATUS_UNSPECIFIED}
|
||||
tooltipContent={
|
||||
<>
|
||||
<p className="mb-2">{primaryTooltip}</p>
|
||||
<p className="mb-2">{secondaryTooltip}</p>
|
||||
<p className="mb-2">
|
||||
{t('Status: %s', PositionStatusMapping[data.status])}
|
||||
</p>
|
||||
{POSITION_RESOLUTION_LINK && (
|
||||
<ExternalLink href={POSITION_RESOLUTION_LINK}>
|
||||
{t('Read more about position resolution')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{cellContent}
|
||||
{valueFormatted}
|
||||
</WarningCell>
|
||||
);
|
||||
};
|
||||
|
||||
const WarningCell = ({
|
||||
children,
|
||||
tooltipContent,
|
||||
showIcon = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
tooltipContent: ReactNode;
|
||||
showIcon?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-end items-center">
|
||||
{showIcon && (
|
||||
<span className="text-black dark:text-white mr-2">
|
||||
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />
|
||||
<Tooltip description={tooltipContent}>
|
||||
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
|
||||
<span className="text-black dark:text-white mr-1">
|
||||
{showIcon && <Icon name="warning-sign" size={3} />}
|
||||
</span>
|
||||
)}
|
||||
<span className="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-ellipsis overflow-hidden">{children}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const StackedCell = ({
|
||||
primary,
|
||||
secondary,
|
||||
}: {
|
||||
primary: ReactNode;
|
||||
secondary: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="leading-4 text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<div data-testid="stack-cell-primary">{primary}</div>
|
||||
<div data-testid="stack-cell-secondary" className="text-muted">
|
||||
{secondary}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CenteredGridCellWrapper,
|
||||
COL_DEFS,
|
||||
DateRangeFilter,
|
||||
MarketProductPill,
|
||||
SetFilter,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import compact from 'lodash/compact';
|
||||
@@ -19,15 +20,13 @@ import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ProposalProductTypeMapping,
|
||||
ProposalProductTypeShortName,
|
||||
ProposalStateMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { InstrumentConfiguration } from '@vegaprotocol/types';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
||||
import { VoteProgress } from '../voting-progress';
|
||||
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
|
||||
import { getMarketProductType } from '../../utils/get-market-product-type';
|
||||
|
||||
export const MarketNameProposalCell = ({
|
||||
value,
|
||||
@@ -39,19 +38,13 @@ export const MarketNameProposalCell = ({
|
||||
const { VEGA_TOKEN_URL } = useEnvironment();
|
||||
const { change } = data?.terms || {};
|
||||
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
|
||||
const type = change.instrument.futureProduct?.__typename;
|
||||
const productType = getMarketProductType(
|
||||
change.instrument as InstrumentConfiguration
|
||||
);
|
||||
const content = (
|
||||
<>
|
||||
<span data-testid="market-code">{value as string}</span>
|
||||
{type && (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProposalProductTypeMapping[type]}
|
||||
>
|
||||
{ProposalProductTypeShortName[type]}
|
||||
</Pill>
|
||||
)}
|
||||
<MarketProductPill productType={productType} />
|
||||
</>
|
||||
);
|
||||
if (data?.id) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { InstrumentConfiguration } from '@vegaprotocol/types';
|
||||
import { getMarketProductType } from './get-market-product-type';
|
||||
|
||||
describe('getMarketProductType', () => {
|
||||
it('should resolve product type properly', () => {
|
||||
expect(
|
||||
getMarketProductType({
|
||||
futureProduct: {
|
||||
quoteName: 'Market 1',
|
||||
},
|
||||
} as InstrumentConfiguration)
|
||||
).toEqual('Future');
|
||||
expect(
|
||||
getMarketProductType({
|
||||
spotProduct: {
|
||||
quoteName: 'Market 1',
|
||||
},
|
||||
} as unknown as InstrumentConfiguration)
|
||||
).toEqual('Spot');
|
||||
expect(
|
||||
getMarketProductType({
|
||||
perpetualProduct: {
|
||||
quoteName: 'Market 1',
|
||||
},
|
||||
} as unknown as InstrumentConfiguration)
|
||||
).toEqual('Perpetual');
|
||||
expect(
|
||||
getMarketProductType({
|
||||
product: {
|
||||
__typename: 'Perpetual',
|
||||
},
|
||||
futureProduct: {
|
||||
quoteName: 'Market 1',
|
||||
},
|
||||
} as unknown as InstrumentConfiguration)
|
||||
).toEqual('Perpetual');
|
||||
expect(
|
||||
getMarketProductType({
|
||||
product: {
|
||||
__typename: 'Spot',
|
||||
},
|
||||
futureProduct: {
|
||||
quoteName: 'Market 1',
|
||||
},
|
||||
} as unknown as InstrumentConfiguration)
|
||||
).toEqual('Spot');
|
||||
expect(
|
||||
getMarketProductType({
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
},
|
||||
perpetualProduct: {
|
||||
quoteName: 'Market 1',
|
||||
},
|
||||
} as unknown as InstrumentConfiguration)
|
||||
).toEqual('Future');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { InstrumentConfiguration, Product } from '@vegaprotocol/types';
|
||||
|
||||
// it needs to be adjusted after deploy this https://github.com/vegaprotocol/vega/pull/9003
|
||||
export const getMarketProductType = (
|
||||
instrumentConfiguration: InstrumentConfiguration
|
||||
) => {
|
||||
return 'product' in instrumentConfiguration
|
||||
? (instrumentConfiguration.product as Product).__typename
|
||||
: 'futureProduct' in instrumentConfiguration
|
||||
? 'Future'
|
||||
: 'spotProduct' in instrumentConfiguration
|
||||
? 'Spot'
|
||||
: 'perpetualProduct' in instrumentConfiguration
|
||||
? 'Perpetual'
|
||||
: undefined;
|
||||
};
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
DispatchMetric,
|
||||
StopOrderStatus,
|
||||
} from './__generated__/types';
|
||||
import type { ProductType, ProposalProductType } from './product';
|
||||
|
||||
export const AccountTypeMapping: {
|
||||
[T in AccountType]: string;
|
||||
@@ -514,20 +513,3 @@ export const PeggedReferenceMapping: { [R in PeggedReference]: string } = {
|
||||
PEGGED_REFERENCE_BEST_BID: 'Bid',
|
||||
PEGGED_REFERENCE_MID: 'Mid',
|
||||
};
|
||||
|
||||
export const ProductTypeMapping: Record<ProductType, string> = {
|
||||
Future: 'Future',
|
||||
};
|
||||
|
||||
export const ProductTypeShortName: Record<ProductType, string> = {
|
||||
Future: 'Futr',
|
||||
};
|
||||
|
||||
export const ProposalProductTypeMapping: Record<ProposalProductType, string> = {
|
||||
FutureProduct: 'Future',
|
||||
};
|
||||
|
||||
export const ProposalProductTypeShortName: Record<ProposalProductType, string> =
|
||||
{
|
||||
FutureProduct: 'Futr',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './__generated__/types';
|
||||
export * from './candle';
|
||||
export * from './global-types-mappings';
|
||||
export * from './product';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { Product } from './__generated__/types';
|
||||
|
||||
export type ProductType = NonNullable<Product['__typename']>;
|
||||
|
||||
// TODO: Update to be dynamically created for ProductionConfiguration union when schema
|
||||
// changes make it to stagnet1
|
||||
export type ProposalProductType = 'FutureProduct';
|
||||
@@ -60,9 +60,5 @@ export const Tooltip = ({
|
||||
);
|
||||
|
||||
export const TooltipCellComponent = (props: ITooltipParams) => {
|
||||
return (
|
||||
<div className={tooltipContentClasses} role="tooltip">
|
||||
{props.value}
|
||||
</div>
|
||||
);
|
||||
return <p className={tooltipContentClasses}>{props.value}</p>;
|
||||
};
|
||||
|
||||
@@ -44,8 +44,6 @@ describe('number utils', () => {
|
||||
o: '12,345,678.91234568',
|
||||
q: '1',
|
||||
},
|
||||
// USDT / USDC
|
||||
{ v: new BigNumber(12345678), d: 6, o: '12.35', q: 1000000 },
|
||||
])(
|
||||
'formats with addDecimalsFormatNumberQuantum given number correctly',
|
||||
({ v, d, o, q }) => {
|
||||
|
||||
@@ -4,37 +4,30 @@
|
||||
|
||||
- **Must** be able to view the following columns (<a name="7004-POSI-002" href="#7004-POSI-002">7004-POSI-002</a>):
|
||||
|
||||
- Market
|
||||
- Code
|
||||
- Settlement asset symbol
|
||||
- Product type
|
||||
- Size / Notional
|
||||
- Size
|
||||
- Notional
|
||||
- Entry / Mark
|
||||
- Entry
|
||||
- Mark
|
||||
- Margin
|
||||
- Allocated
|
||||
- Leverage
|
||||
- Liquidation
|
||||
- Realised PNL
|
||||
- Unrealised PNL
|
||||
- market
|
||||
- notional
|
||||
- open volume
|
||||
- mark price
|
||||
- settlement asset
|
||||
- entry price
|
||||
- leverage
|
||||
- margin allocated
|
||||
- realised PNL
|
||||
- unrealised PNL
|
||||
- Updated
|
||||
|
||||
- **Must** be able to sort each column by asc and dsc (<a name="7004-POSI-003" href="#7004-POSI-003">7004-POSI-003</a>)
|
||||
|
||||
- If the cell shows two values the primary (top value) is used to sort (<a name="7004-POSI-014" href="#7004-POSI-014">7004-POSI-014</a>)
|
||||
|
||||
- **Must** be able to scroll horizontally to see all columns if screen resolution isn't wide enough (<a name="7004-POSI-004" href="#7004-POSI-004">7004-POSI-004</a>)
|
||||
|
||||
- **Must** be able to drag and drop column names to re order (<a name="7004-POSI-005" href="#7004-POSI-005">7004-POSI-005</a>)
|
||||
|
||||
- **Must** be able to remove columns by dragging headers out of the table (<a name="7004-POSI-015" href="#7004-POSI-015">7004-POSI-015</a>)
|
||||
|
||||
- **Must** be able to resize the width of the columns (<a name="7004-POSI-006" href="#7004-POSI-006">7004-POSI-006</a>)
|
||||
|
||||
- **Must** The columns 'Open volume', 'realised PNL' and 'Unrealised PNL' must change color and have a + or - suffix depending on being positive/negative (<a name="7004-POSI-007" href="#7004-POSI-007">7004-POSI-007</a>)
|
||||
|
||||
- **Must** be able to select settlement asset to view asset details (<a name="7004-POSI-008" href="#7004-POSI-008">7004-POSI-008</a>)
|
||||
|
||||
- **Must** be able to select the 3 dots to view asset details (<a name="7004-POSI-009" href="#7004-POSI-009">7004-POSI-009</a>)
|
||||
|
||||
- **Must** be able to select the close button to close my position (<a name="7004-POSI-010" href="#7004-POSI-010">7004-POSI-010</a>)
|
||||
@@ -44,15 +37,3 @@
|
||||
- **Must** retain previous column sizing on refresh (<a name="7004-POSI-012" href="#7004-POSI-012">7004-POSI-012</a>)
|
||||
|
||||
- **Must** notional column has a tooltip on hover (<a name="7004-POSI-013" href="#7004-POSI-013">7004-POSI-013</a>)
|
||||
|
||||
- **Must** be able to see worst case liquidation price (<a name="7004-POSI-016" href="#7004-POSI-016">7004-POSI-016</a>)
|
||||
|
||||
- **Must** be able to see best and worst case liquidation price when hovering (<a name="7004-POSI-017" href="#7004-POSI-017">7004-POSI-017</a>)
|
||||
|
||||
- **Must** be able to see if your realised PnL was affected by loss socialisation (<a name="7004-POSI-018" href="#7004-POSI-018">7004-POSI-018</a>)
|
||||
|
||||
- **Must** be able to see what type of product the position was opened on (<a name="7004-POSI-019" href="#7004-POSI-019">7004-POSI-019</a>)
|
||||
|
||||
- **Must** not see positions on markets which are closed (<a name="7004-POSI-020" href="#7004-POSI-020">7004-POSI-020</a>)
|
||||
|
||||
- **Must** be able to show closed markets (<a name="7004-POSI-021" href="#7004-POSI-021">7004-POSI-021</a>)
|
||||
|
||||
Reference in New Issue
Block a user