feat(#840): update positions tab (#1101)

* feat(#473): add positions metrics data provider

* feat(#473) add positions stats

* feat(#473) add positions stats

* feat(#473): add positions stats

* feat(#473): add positions stats

* feat(#473): position metrics, test and refactoring

* feat(#473): add unit tests to positions table

* feat(#473): fix spelling, order positions by updated at desc

* feat(#473): protect from division by 0

* feat(#473): fix trading positions e2e tests

* feat(#473): fix e2e data mocks

* feat(#473): post code review clean up

* feat(#993): dependencies handling in data provider

* feat(#993): fix e2e tests data mocks

* feat(#993): remove position metrics mocks, add market data market id

* feat(#993): add missing mocks, fix combine function

* feat(#993): set loading initially to true, add unit tests

* feat(#993): cleanup, add comments

* feat(#993): remove undefined from client type

* feat(#993): cosmetic changes

* feat(#840): update positions tab

* feat:(#993): pass informaton about update callback cause

* feat(#840): update positions tab

* feat(#840): update positions tab

* feat(#840): update positions tab

* chore: skip handles 5000 markets e2e test

* feat(#840): update positions tab

* feat(#840): rename assetDecimals to decimals

* feat(#840): close position

* feat(#993): notify about update

* feat(#840): add use close position hook

* feat(#840): do not show 0 volume positions, make liquidation price minimum 0

* feat(#840): post code review fixes and improvments

* feat: fix fill-table spec
This commit is contained in:
Bartłomiej Głownia 2022-09-02 22:53:00 +02:00 committed by GitHub
parent fe716f74a1
commit 6de90c6b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 946 additions and 535 deletions

View File

@ -5,19 +5,22 @@ import { Stepper } from '../stepper';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit'; import { InputError } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import { import { MarketSelector } from '@vegaprotocol/deal-ticket';
getOrderDialogTitle,
getOrderDialogIntent,
getOrderDialogIcon,
MarketSelector,
} from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders'; import type { Order } from '@vegaprotocol/orders';
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet'; import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers'; import {
t,
addDecimal,
toDecimal,
removeDecimal,
} from '@vegaprotocol/react-helpers';
import { import {
getDefaultOrder, getDefaultOrder,
useOrderValidation, useOrderValidation,
useOrderSubmit, useOrderSubmit,
getOrderDialogTitle,
getOrderDialogIntent,
getOrderDialogIcon,
OrderFeedback, OrderFeedback,
validateSize, validateSize,
} from '@vegaprotocol/orders'; } from '@vegaprotocol/orders';
@ -108,7 +111,7 @@ export const DealTicketSteps = ({
}); });
const { submit, transaction, finalizedOrder, TransactionDialog } = const { submit, transaction, finalizedOrder, TransactionDialog } =
useOrderSubmit(market); useOrderSubmit();
const onSizeChange = (value: number[]) => { const onSizeChange = (value: number[]) => {
const newVal = new BigNumber(value[0]) const newVal = new BigNumber(value[0])
@ -151,10 +154,20 @@ export const DealTicketSteps = ({
const onSubmit = React.useCallback( const onSubmit = React.useCallback(
(order: Order) => { (order: Order) => {
if (transactionStatus !== 'pending') { if (transactionStatus !== 'pending') {
submit(order); submit({
...order,
price:
order.price && removeDecimal(order.price, market.decimalPlaces),
size: removeDecimal(order.size, market.positionDecimalPlaces),
});
} }
}, },
[transactionStatus, submit] [
transactionStatus,
submit,
market.decimalPlaces,
market.positionDecimalPlaces,
]
); );
const steps = [ const steps = [

View File

@ -25,7 +25,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
({ delta }: { delta: AccountSubscribe_accounts }) => { ({ delta }: { delta: AccountSubscribe_accounts }) => {
const update: Accounts_party_accounts[] = []; const update: Accounts_party_accounts[] = [];
const add: Accounts_party_accounts[] = []; const add: Accounts_party_accounts[] = [];
if (!gridRef.current) { if (!gridRef.current?.api) {
return false; return false;
} }
const rowNode = gridRef.current.api.getRowNode(getId(delta)); const rowNode = gridRef.current.api.getRowNode(getId(delta));

View File

@ -0,0 +1,42 @@
import { useMemo } from 'react';
import {
addDecimalsFormatNumber,
useDataProvider,
} from '@vegaprotocol/react-helpers';
import type { AccountSubscribe_accounts } from './__generated__/AccountSubscribe';
import type { Accounts_party_accounts } from './__generated__/Accounts';
import { accountsDataProvider } from './accounts-data-provider';
interface AssetBalanceProps {
partyId: string;
assetSymbol: string;
}
export const AssetBalance = ({ partyId, assetSymbol }: AssetBalanceProps) => {
const variables = useMemo(() => ({ partyId }), [partyId]);
const { data } = useDataProvider<
Accounts_party_accounts[],
AccountSubscribe_accounts
>({
dataProvider: accountsDataProvider,
variables,
});
if (data && data.length) {
const totalBalance = data.reduce((a, c) => {
if (c.asset.symbol === assetSymbol) {
return a + BigInt(c.balance);
}
return a;
}, BigInt(0));
return (
<span>
{addDecimalsFormatNumber(
totalBalance.toString(),
data[0].asset.decimals
)}
</span>
);
}
return null;
};

View File

@ -3,3 +3,4 @@ export * from './accounts-container';
export * from './accounts-data-provider'; export * from './accounts-data-provider';
export * from './accounts-manager'; export * from './accounts-manager';
export * from './accounts-table'; export * from './accounts-table';
export * from './asset-balance';

View File

@ -17,7 +17,7 @@ export const DealTicketManager = ({
children, children,
}: DealTicketManagerProps) => { }: DealTicketManagerProps) => {
const { submit, transaction, finalizedOrder, TransactionDialog } = const { submit, transaction, finalizedOrder, TransactionDialog } =
useOrderSubmit(market); useOrderSubmit();
return ( return (
<> <>

View File

@ -1,6 +1,10 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { t, addDecimalsFormatNumber } from '@vegaprotocol/react-helpers'; import {
t,
addDecimalsFormatNumber,
removeDecimal,
} from '@vegaprotocol/react-helpers';
import { Button, InputError } from '@vegaprotocol/ui-toolkit'; import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { TypeSelector } from './type-selector'; import { TypeSelector } from './type-selector';
import { SideSelector } from './side-selector'; import { SideSelector } from './side-selector';
@ -51,10 +55,15 @@ export const DealTicket = ({
const onSubmit = useCallback( const onSubmit = useCallback(
(order: Order) => { (order: Order) => {
if (!isDisabled) { if (!isDisabled) {
submit(order); submit({
...order,
price:
order.price && removeDecimal(order.price, market.decimalPlaces),
size: removeDecimal(order.size, market.positionDecimalPlaces),
});
} }
}, },
[isDisabled, submit] [isDisabled, submit, market.decimalPlaces, market.positionDecimalPlaces]
); );
return ( return (
@ -111,7 +120,7 @@ export const DealTicket = ({
{orderType === OrderType.TYPE_LIMIT && {orderType === OrderType.TYPE_LIMIT &&
orderTimeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT && ( orderTimeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller <Controller
name="expiration" name="expiresAt"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<ExpirySelector value={field.value} onSelect={field.onChange} /> <ExpirySelector value={field.value} onSelect={field.onChange} />

View File

@ -1,4 +1,4 @@
import { render, screen, waitFor, act } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers'; import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
@ -141,7 +141,6 @@ describe('FillsTable', () => {
}); });
it('should format cells correctly for seller fill', async () => { it('should format cells correctly for seller fill', async () => {
act(async () => {
const partyId = 'party-id'; const partyId = 'party-id';
const buyerFill = generateFill({ const buyerFill = generateFill({
...defaultFill, ...defaultFill,
@ -175,12 +174,10 @@ describe('FillsTable', () => {
}); });
const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size'); const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size');
expect(amountCell).toHaveClass('text-vega-red'); expect(amountCell).toHaveClass('text-vega-red-dark');
});
}); });
it('should render correct maker or taker role', async () => { it('should render correct maker or taker role', async () => {
act(async () => {
const partyId = 'party-id'; const partyId = 'party-id';
const takerFill = generateFill({ const takerFill = generateFill({
seller: { seller: {
@ -219,4 +216,3 @@ describe('FillsTable', () => {
).toHaveTextContent('Maker'); ).toHaveTextContent('Maker');
}); });
}); });
});

View File

@ -4,6 +4,8 @@ import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumber, formatNumber,
getDateTimeFormat, getDateTimeFormat,
positiveClassNames,
negativeClassNames,
t, t,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
@ -49,9 +51,8 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
field="size" field="size"
cellClass={({ data }: { data: FillFields }) => { cellClass={({ data }: { data: FillFields }) => {
return classNames('text-right', { return classNames('text-right', {
'text-vega-green-dark dark:text-vega-green': [positiveClassNames]: data?.buyer.id === partyId,
data?.buyer.id === partyId, [negativeClassNames]: data?.seller.id,
'text-vega-red-dark dark:text-vega-red': data?.seller.id,
}); });
}} }}
valueFormatter={formatSize(partyId)} valueFormatter={formatSize(partyId)}

View File

@ -1,6 +1,11 @@
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import type { OrderEvent_busEvents_event_Order } from '../../order-hooks/__generated__'; import type { OrderEvent_busEvents_event_Order } from '../../order-hooks/__generated__';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; import {
addDecimalsFormatNumber,
t,
positiveClassNames,
negativeClassNames,
} from '@vegaprotocol/react-helpers';
import { import {
OrderRejectionReasonMapping, OrderRejectionReasonMapping,
OrderStatus, OrderStatus,
@ -49,8 +54,8 @@ export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {
<p <p
className={ className={
order.side === Side.SIDE_BUY order.side === Side.SIDE_BUY
? 'text-vega-green-dark dark:text-vega-green' ? positiveClassNames
: 'text-vega-red-dark dark:text-vega-red' : negativeClassNames
} }
> >
{`${ {`${

View File

@ -8,7 +8,13 @@ import {
OrderTimeInForceMapping, OrderTimeInForceMapping,
OrderRejectionReasonMapping, OrderRejectionReasonMapping,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { addDecimal, getDateTimeFormat, t } from '@vegaprotocol/react-helpers'; import {
addDecimal,
getDateTimeFormat,
t,
positiveClassNames,
negativeClassNames,
} from '@vegaprotocol/react-helpers';
import { import {
AgGridDynamic as AgGrid, AgGridDynamic as AgGrid,
Button, Button,
@ -125,12 +131,12 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
cellClass="font-mono text-right" cellClass="font-mono text-right"
type="rightAligned" type="rightAligned"
cellClassRules={{ cellClassRules={{
'text-vega-green-dark dark:text-vega-green': ({ [positiveClassNames]: ({
data, data,
}: { }: {
data: Orders_party_ordersConnection_edges_node; data: Orders_party_ordersConnection_edges_node;
}) => data?.side === Side.SIDE_BUY, }) => data?.side === Side.SIDE_BUY,
'text-vega-red-dark dark:text-vega-red': ({ [negativeClassNames]: ({
data, data,
}: { }: {
data: Orders_party_ordersConnection_edges_node; data: Orders_party_ordersConnection_edges_node;

View File

@ -60,10 +60,7 @@ const defaultWalletContext = {
connector: null, connector: null,
}; };
function setup( function setup(context?: Partial<VegaWalletContextShape>) {
context?: Partial<VegaWalletContextShape>,
market = defaultMarket
) {
const mocks: MockedResponse<OrderEvent> = { const mocks: MockedResponse<OrderEvent> = {
request: { request: {
query: ORDER_EVENT_SUB, query: ORDER_EVENT_SUB,
@ -144,7 +141,7 @@ function setup(
</VegaWalletContext.Provider> </VegaWalletContext.Provider>
</MockedProvider> </MockedProvider>
); );
return renderHook(() => useOrderSubmit(market), { wrapper }); return renderHook(() => useOrderSubmit(), { wrapper });
} }
describe('useOrderSubmit', () => { describe('useOrderSubmit', () => {
@ -164,11 +161,11 @@ describe('useOrderSubmit', () => {
size: '10', size: '10',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
side: Side.SIDE_BUY, side: Side.SIDE_BUY,
price: '1234567.89', price: '123456789',
expiration: new Date('2022-01-01'), expiresAt: new Date('2022-01-01'),
}; };
await act(async () => { await act(async () => {
result.current.submit(order); result.current.submit({ ...order, marketId: defaultMarket.id });
}); });
expect(mockSendTx).toHaveBeenCalledWith({ expect(mockSendTx).toHaveBeenCalledWith({
@ -176,14 +173,12 @@ describe('useOrderSubmit', () => {
propagate: true, propagate: true,
orderSubmission: { orderSubmission: {
type: OrderType.TYPE_LIMIT, type: OrderType.TYPE_LIMIT,
marketId: defaultMarket.id, // Market provided from hook argument marketId: defaultMarket.id,
size: '100', // size adjusted based on positionDecimalPlaces size: '10',
side: Side.SIDE_BUY, side: Side.SIDE_BUY,
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
price: '123456789', // Decimal removed price: '123456789',
expiresAt: order.expiration expiresAt: order.expiresAt ? toNanoSeconds(order.expiresAt) : undefined,
? toNanoSeconds(order.expiration)
: undefined,
}, },
}); });
}); });

View File

@ -1,103 +0,0 @@
import { useCallback, useState } from 'react';
import type { OrderEvent_busEvents_event_Order } from './__generated__';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
determineId,
removeDecimal,
toNanoSeconds,
} from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
import * as Sentry from '@sentry/react';
import { useOrderEvent } from './use-order-event';
import type { OrderTimeInForce, Side } from '@vegaprotocol/types';
import { OrderType } from '@vegaprotocol/types';
export interface Order {
type: OrderType;
size: string;
side: Side;
timeInForce: OrderTimeInForce;
price?: string;
expiration?: Date;
}
export interface Market {
id: string;
decimalPlaces: number;
positionDecimalPlaces: number;
}
export const useOrderSubmit = (market: Market) => {
const { keypair } = useVegaWallet();
const waitForOrderEvent = useOrderEvent();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
TransactionDialog,
} = useVegaTransaction();
const [finalizedOrder, setFinalizedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
}, [resetTransaction]);
const submit = useCallback(
async (order: Order) => {
if (!keypair || !order.side) {
return;
}
setFinalizedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderSubmission: {
marketId: market.id,
price:
order.type === OrderType.TYPE_LIMIT && order.price
? removeDecimal(order.price, market.decimalPlaces)
: undefined,
size: removeDecimal(order.size, market.positionDecimalPlaces),
type: order.type,
side: order.side,
timeInForce: order.timeInForce,
expiresAt: order.expiration
? toNanoSeconds(order.expiration) // Wallet expects timestamp in nanoseconds
: undefined,
},
});
if (res?.signature) {
const resId = determineId(res.signature);
if (resId) {
waitForOrderEvent(resId, keypair.pub, (order) => {
setFinalizedOrder(order);
setComplete();
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[keypair, send, market, setComplete, waitForOrderEvent]
);
return {
transaction,
finalizedOrder,
TransactionDialog,
submit,
reset,
};
};

View File

@ -0,0 +1,165 @@
import { useCallback, useState } from 'react';
import type { ReactNode } from 'react';
import type { OrderEvent_busEvents_event_Order } from './__generated__';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { determineId, toNanoSeconds } from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
import * as Sentry from '@sentry/react';
import { useOrderEvent } from './use-order-event';
import type { OrderTimeInForce, Side } from '@vegaprotocol/types';
import { OrderType, OrderStatus } from '@vegaprotocol/types';
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
export interface Order {
marketId: string;
type: OrderType;
size: string;
side: Side;
timeInForce: OrderTimeInForce;
price?: string;
expiresAt?: Date;
}
export const getOrderDialogTitle = (
status?: OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.STATUS_ACTIVE:
return t('Order submitted');
case OrderStatus.STATUS_FILLED:
return t('Order filled');
case OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case OrderStatus.STATUS_PARKED:
return t('Order parked');
case OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Submission failed');
}
};
export const getOrderDialogIntent = (
status?: OrderStatus
): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.STATUS_PARKED:
case OrderStatus.STATUS_EXPIRED:
case OrderStatus.STATUS_PARTIALLY_FILLED:
return Intent.Warning;
case OrderStatus.STATUS_REJECTED:
case OrderStatus.STATUS_STOPPED:
case OrderStatus.STATUS_CANCELLED:
return Intent.Danger;
case OrderStatus.STATUS_FILLED:
case OrderStatus.STATUS_ACTIVE:
return Intent.Success;
default:
return;
}
};
export const getOrderDialogIcon = (
status?: OrderStatus
): ReactNode | undefined => {
if (!status) {
return;
}
switch (status) {
case OrderStatus.STATUS_PARKED:
case OrderStatus.STATUS_EXPIRED:
return <Icon name="warning-sign" size={16} />;
case OrderStatus.STATUS_REJECTED:
case OrderStatus.STATUS_STOPPED:
case OrderStatus.STATUS_CANCELLED:
return <Icon name="error" size={16} />;
default:
return;
}
};
export const useOrderSubmit = () => {
const { keypair } = useVegaWallet();
const waitForOrderEvent = useOrderEvent();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
TransactionDialog,
} = useVegaTransaction();
const [finalizedOrder, setFinalizedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
}, [resetTransaction]);
const submit = useCallback(
async (order: Order) => {
if (!keypair || !order.side) {
return;
}
setFinalizedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderSubmission: {
...order,
price:
order.type === OrderType.TYPE_LIMIT && order.price
? order.price
: undefined,
expiresAt: order.expiresAt
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
: undefined,
},
});
if (res?.signature) {
const resId = determineId(res.signature);
if (resId) {
waitForOrderEvent(resId, keypair.pub, (order) => {
setFinalizedOrder(order);
setComplete();
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[keypair, send, setComplete, waitForOrderEvent]
);
return {
transaction,
finalizedOrder,
TransactionDialog,
submit,
reset,
};
};

View File

@ -1,8 +1,12 @@
import { toDecimal } from '@vegaprotocol/react-helpers'; import { toDecimal } from '@vegaprotocol/react-helpers';
import type { Order, Market } from '../order-hooks'; import type { Order } from '../order-hooks';
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types'; import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
export const getDefaultOrder = (market: Market): Order => ({ export const getDefaultOrder = (market: {
id: string;
positionDecimalPlaces: number;
}): Order => ({
marketId: market.id,
type: OrderType.TYPE_MARKET, type: OrderType.TYPE_MARKET,
side: Side.SIDE_BUY, side: Side.SIDE_BUY,
timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC,

View File

@ -1,4 +1,6 @@
export * from './lib/positions-table'; export * from './lib/__generated__/Positions';
export * from './lib/positions-container'; export * from './lib/positions-container';
export * from './lib/positions-data-providers'; export * from './lib/positions-data-providers';
export * from './lib/__generated__/Positions'; export * from './lib/positions-table';
export * from './lib/use-close-position';
export * from './lib/use-position-event';

View File

@ -197,7 +197,7 @@ describe('getMetrics', () => {
expect(metrics[0].currentLeverage).toBeCloseTo(1.02); expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5); expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0); expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].assetDecimals).toEqual(5); expect(metrics[0].decimals).toEqual(5);
expect(metrics[0].liquidationPrice).toEqual('169990'); expect(metrics[0].liquidationPrice).toEqual('169990');
expect(metrics[0].lowMarginLevel).toEqual(false); expect(metrics[0].lowMarginLevel).toEqual(false);
expect(metrics[0].markPrice).toEqual('9431775'); expect(metrics[0].markPrice).toEqual('9431775');
@ -222,7 +222,7 @@ describe('getMetrics', () => {
expect(metrics[1].currentLeverage).toBeCloseTo(0.097); expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5); expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0); expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].assetDecimals).toEqual(5); expect(metrics[1].decimals).toEqual(5);
expect(metrics[1].liquidationPrice).toEqual('9830750'); expect(metrics[1].liquidationPrice).toEqual('9830750');
expect(metrics[1].lowMarginLevel).toEqual(false); expect(metrics[1].lowMarginLevel).toEqual(false);
expect(metrics[1].markPrice).toEqual('869762'); expect(metrics[1].markPrice).toEqual('869762');

View File

@ -24,7 +24,7 @@ export interface Position {
averageEntryPrice: string; averageEntryPrice: string;
capitalUtilisation: number; capitalUtilisation: number;
currentLeverage: number; currentLeverage: number;
assetDecimals: number; decimals: number;
marketDecimalPlaces: number; marketDecimalPlaces: number;
positionDecimalPlaces: number; positionDecimalPlaces: number;
totalBalance: string; totalBalance: string;
@ -133,7 +133,12 @@ export const getMetrics = (
const marginAccount = accounts?.find((account) => { const marginAccount = accounts?.find((account) => {
return account.market?.id === market.id; return account.market?.id === market.id;
}); });
if (!marginAccount || !marginLevel || !marketData) { if (
!marginAccount ||
!marginLevel ||
!marketData ||
position.node.openVolume === '0'
) {
return; return;
} }
const generalAccount = accounts?.find( const generalAccount = accounts?.find(
@ -141,7 +146,7 @@ export const getMetrics = (
account.asset.id === marginAccount.asset.id && account.asset.id === marginAccount.asset.id &&
account.type === AccountType.ACCOUNT_TYPE_GENERAL account.type === AccountType.ACCOUNT_TYPE_GENERAL
); );
const assetDecimals = marginAccount.asset.decimals; const decimals = marginAccount.asset.decimals;
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } = const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market; market;
const openVolume = toBigNum( const openVolume = toBigNum(
@ -149,13 +154,10 @@ export const getMetrics = (
positionDecimalPlaces positionDecimalPlaces
); );
const marginAccountBalance = toBigNum( const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals);
marginAccount.balance ?? 0,
assetDecimals
);
const generalAccountBalance = toBigNum( const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0, generalAccount?.balance ?? 0,
assetDecimals decimals
); );
const markPrice = toBigNum(marketData.markPrice, marketDecimalPlaces); const markPrice = toBigNum(marketData.markPrice, marketDecimalPlaces);
@ -180,19 +182,19 @@ export const getMetrics = (
marketDecimalPlaces marketDecimalPlaces
); );
const searchPrice = openVolume.isEqualTo(0) const searchPrice = marginSearch
? markPrice
: marginSearch
.minus(marginAccountBalance) .minus(marginAccountBalance)
.dividedBy(openVolume) .dividedBy(openVolume)
.plus(markPrice); .plus(markPrice);
const liquidationPrice = openVolume.isEqualTo(0)
? markPrice const liquidationPrice = BigNumber.maximum(
: marginMaintenance 0,
marginMaintenance
.minus(marginAccountBalance) .minus(marginAccountBalance)
.minus(generalAccountBalance) .minus(generalAccountBalance)
.dividedBy(openVolume) .dividedBy(openVolume)
.plus(markPrice); .plus(markPrice)
);
const lowMarginLevel = const lowMarginLevel =
marginAccountBalance.isLessThan( marginAccountBalance.isLessThan(
@ -206,9 +208,9 @@ export const getMetrics = (
currentLeverage: currentLeverage.toNumber(), currentLeverage: currentLeverage.toNumber(),
marketDecimalPlaces, marketDecimalPlaces,
positionDecimalPlaces, positionDecimalPlaces,
assetDecimals, decimals,
assetSymbol: marginLevel.asset.symbol, assetSymbol: marginLevel.asset.symbol,
totalBalance: totalBalance.multipliedBy(10 ** assetDecimals).toFixed(), totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
lowMarginLevel, lowMarginLevel,
liquidationPrice: liquidationPrice liquidationPrice: liquidationPrice
.multipliedBy(10 ** marketDecimalPlaces) .multipliedBy(10 ** marketDecimalPlaces)

View File

@ -1,26 +1,40 @@
import { useRef, useCallback, useMemo } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { AgGridReact } from 'ag-grid-react';
import PositionsTable from './positions-table';
import type { GetRowsParams } from './positions-table';
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers'; import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import { Positions } from './positions';
import { useClosePosition } from '../';
interface PositionsManagerProps { interface PositionsManagerProps {
partyId: string; partyId: string;
} }
const getSymbols = (positions: Position[]) =>
Array.from(new Set(positions.map((position) => position.assetSymbol))).sort();
export const PositionsManager = ({ partyId }: PositionsManagerProps) => { export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
const dataRef = useRef<Position[] | null>(null); const assetSymbols = useRef<string[] | undefined>();
const { submit, TransactionDialog } = useClosePosition();
const onClose = useCallback(
(position: Position) => {
submit(position);
},
[submit]
);
const update = useCallback(({ data }: { data: Position[] | null }) => { const update = useCallback(({ data }: { data: Position[] | null }) => {
if (!gridRef.current?.api) { if (data?.length) {
const newAssetSymbols = getSymbols(data);
if (
!newAssetSymbols.every(
(symbol) =>
assetSymbols.current && assetSymbols.current.includes(symbol)
)
) {
return false; return false;
} }
dataRef.current = data; }
gridRef.current.api.refreshInfiniteCache();
return true; return true;
}, []); }, []);
const { data, error, loading } = useDataProvider<Position[], never>({ const { data, error, loading } = useDataProvider<Position[], never>({
@ -28,26 +42,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
update, update,
variables, variables,
}); });
dataRef.current = data;
const getRows = async ({
successCallback,
startRow,
endRow,
}: GetRowsParams) => {
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current?.length ?? -1;
successCallback(rowsThisBlock, lastRow);
};
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <>
<PositionsTable <AsyncRenderer loading={loading} error={error} data={assetSymbols}>
rowModelType={data?.length ? 'infinite' : 'clientSide'} {data &&
rowData={data?.length ? undefined : []} getSymbols(data)?.map((assetSymbol) => (
datasource={{ getRows }} <Positions
partyId={partyId}
assetSymbol={assetSymbol}
key={assetSymbol}
onClose={onClose}
/> />
))}
</AsyncRenderer> </AsyncRenderer>
<TransactionDialog>
<p>Your position was not closed! This is still not implemented. </p>
</TransactionDialog>
</>
); );
}; };

View File

@ -11,7 +11,7 @@ const singleRow: Position = {
currentLeverage: 1.1, currentLeverage: 1.1,
marketDecimalPlaces: 1, marketDecimalPlaces: 1,
positionDecimalPlaces: 0, positionDecimalPlaces: 0,
assetDecimals: 2, decimals: 2,
totalBalance: '123456', totalBalance: '123456',
assetSymbol: 'BTC', assetSymbol: 'BTC',
liquidationPrice: '83', // 8.3 liquidationPrice: '83', // 8.3

View File

@ -16,7 +16,7 @@ const longPosition: Position = {
averageEntryPrice: '1134564', averageEntryPrice: '1134564',
capitalUtilisation: 10, capitalUtilisation: 10,
currentLeverage: 11, currentLeverage: 11,
assetDecimals: 2, decimals: 2,
marketDecimalPlaces: 2, marketDecimalPlaces: 2,
positionDecimalPlaces: 2, positionDecimalPlaces: 2,
// generalAccountBalance: '0', // generalAccountBalance: '0',
@ -48,7 +48,7 @@ const shortPosition: Position = {
averageEntryPrice: '23976', averageEntryPrice: '23976',
capitalUtilisation: 87, capitalUtilisation: 87,
currentLeverage: 7, currentLeverage: 7,
assetDecimals: 2, decimals: 2,
marketDecimalPlaces: 2, marketDecimalPlaces: 2,
positionDecimalPlaces: 2, positionDecimalPlaces: 2,
// generalAccountBalance: '0', // generalAccountBalance: '0',

View File

@ -1,6 +1,12 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { ValueFormatterParams } from 'ag-grid-community'; import type { CSSProperties } from 'react';
import type {
ValueFormatterParams,
ValueGetterParams,
ICellRendererParams,
CellRendererSelectorResult,
} from 'ag-grid-community';
import { import {
PriceFlashCell, PriceFlashCell,
addDecimalsFormatNumber, addDecimalsFormatNumber,
@ -8,6 +14,8 @@ import {
t, t,
formatNumber, formatNumber,
getDateTimeFormat, getDateTimeFormat,
signedNumberCssClass,
signedNumberCssClassRules,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
@ -15,7 +23,7 @@ import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community'; import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import { MarketTradingMode } from '@vegaprotocol/types'; import { MarketTradingMode } from '@vegaprotocol/types';
import { Intent } from '@vegaprotocol/ui-toolkit'; import { Intent, Button } from '@vegaprotocol/ui-toolkit';
export const getRowId = ({ data }: { data: Position }) => data.marketId; export const getRowId = ({ data }: { data: Position }) => data.marketId;
@ -29,6 +37,8 @@ export interface Datasource extends IDatasource {
interface Props extends AgGridReactProps { interface Props extends AgGridReactProps {
rowData?: Position[] | null; rowData?: Position[] | null;
datasource?: Datasource; datasource?: Datasource;
onClose?: (data: Position) => void;
style?: CSSProperties;
} }
type PositionsTableValueFormatterParams = Omit< type PositionsTableValueFormatterParams = Omit<
@ -43,12 +53,15 @@ export interface MarketNameCellProps {
} }
export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => { export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
return valueFormatted ? ( if (valueFormatted && valueFormatted[1]) {
return (
<div className="leading-tight"> <div className="leading-tight">
<div>{valueFormatted[0]}</div> <div>{valueFormatted[0]}</div>
{valueFormatted[1] ? <div>{valueFormatted[1]}</div> : null} <div>{valueFormatted[1]}</div>
</div> </div>
) : null; );
}
return (valueFormatted && valueFormatted[0]) || undefined;
}; };
export interface PriceCellProps { export interface PriceCellProps {
@ -63,14 +76,14 @@ export interface PriceCellProps {
export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => { export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
return valueFormatted ? ( return valueFormatted ? (
<> <>
<div className="flex justify-between leading-tight"> <div className="flex justify-between leading-tight font-mono">
<div>{valueFormatted.low}</div> <div>{valueFormatted.low}</div>
<div>{valueFormatted.high}</div> <div>{valueFormatted.high}</div>
</div> </div>
<ProgressBar <ProgressBar
value={valueFormatted.value} value={valueFormatted.value}
intent={valueFormatted.intent} intent={valueFormatted.intent}
className="mt-4" className="mt-2"
/> />
</> </>
) : null; ) : null;
@ -91,14 +104,10 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
} }
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } = const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
valueFormatted; valueFormatted;
const isShortPosition = openVolume.startsWith('-');
return valueFormatted ? ( return valueFormatted ? (
<div className="leading-tight"> <div className="leading-tight font-mono">
<div <div
className={classNames('text-right', { className={classNames('text-right', signedNumberCssClass(openVolume))}
'text-vega-green-dark dark:text-vega-green': !isShortPosition,
'text-vega-red-dark dark:text-vega-red': isShortPosition,
})}
> >
{volumePrefix( {volumePrefix(
addDecimalsFormatNumber(openVolume, positionDecimalPlaces) addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
@ -113,7 +122,24 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell'; AmountCell.displayName = 'AmountCell';
export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => { const ButtonCell = ({
onClick,
data,
}: {
onClick: (position: Position) => void;
data: Position;
}) => {
return (
<Button onClick={() => onClick(data)} size="sm">
{t('Close')}
</Button>
);
};
const EmptyCell = () => '';
export const PositionsTable = forwardRef<AgGridReact, Props>(
({ onClose, ...props }, ref) => {
return ( return (
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
@ -140,10 +166,10 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
if (!value) { if (!value) {
return undefined; return undefined;
} }
// split market name into two parts, 'Part1 (Part2)' // split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2'
const matches = value.match(/^(.*)\((.*)\)\s*$/); const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/);
if (matches) { if (matches) {
return [matches[1].trim(), matches[2].trim()]; return [matches[1].trim(), matches[3].trim()];
} }
return [value]; return [value];
}} }}
@ -151,17 +177,30 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
<AgGridColumn <AgGridColumn
headerName={t('Amount')} headerName={t('Amount')}
field="openVolume" field="openVolume"
valueGetter={({ node, data }: ValueGetterParams) => {
return node?.rowPinned ? data?.notional : data?.openVolume;
}}
type="rightAligned" type="rightAligned"
cellRenderer={AmountCell} cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? PriceFlashCell : AmountCell,
};
}}
valueFormatter={({ valueFormatter={({
value, value,
data, data,
node,
}: PositionsTableValueFormatterParams & { }: PositionsTableValueFormatterParams & {
value: Position['openVolume']; value: Position['openVolume'];
}): AmountCellProps['valueFormatted'] => { }): AmountCellProps['valueFormatted'] | string => {
if (!value || !data) { if (!value || !data) {
return undefined; return undefined;
} }
if (node?.rowPinned) {
return addDecimalsFormatNumber(value, data.decimals);
}
return data; return data;
}} }}
/> />
@ -169,14 +208,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
headerName={t('Mark price')} headerName={t('Mark price')}
field="markPrice" field="markPrice"
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
};
}}
valueFormatter={({ valueFormatter={({
value, value,
data, data,
node,
}: PositionsTableValueFormatterParams & { }: PositionsTableValueFormatterParams & {
value: Position['markPrice']; value: Position['markPrice'];
}) => { }) => {
if (!data) { if (!data || !value || node?.rowPinned) {
return undefined; return undefined;
} }
if ( if (
@ -202,13 +248,20 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
'</div>', '</div>',
}} }}
flex={2} flex={2}
cellRenderer="ProgressBarCell" cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
};
}}
valueFormatter={({ valueFormatter={({
data, data,
node,
}: PositionsTableValueFormatterParams): }: PositionsTableValueFormatterParams):
| PriceCellProps['valueFormatted'] | PriceCellProps['valueFormatted']
| undefined => { | undefined => {
if (!data) { if (!data || node?.rowPinned) {
return undefined; return undefined;
} }
const min = BigInt(data.averageEntryPrice); const min = BigInt(data.averageEntryPrice);
@ -233,9 +286,16 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
headerName={t('Leverage')} headerName={t('Leverage')}
field="currentLeverage" field="currentLeverage"
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
};
}}
valueFormatter={({ valueFormatter={({
value, value,
node,
}: PositionsTableValueFormatterParams & { }: PositionsTableValueFormatterParams & {
value: Position['currentLeverage']; value: Position['currentLeverage'];
}) => }) =>
@ -248,21 +308,26 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
type="rightAligned" type="rightAligned"
flex={2} flex={2}
cellRenderer="ProgressBarCell" cellRenderer="ProgressBarCell"
cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
};
}}
valueFormatter={({ valueFormatter={({
data, data,
value, value,
node,
}: PositionsTableValueFormatterParams & { }: PositionsTableValueFormatterParams & {
value: Position['capitalUtilisation']; value: Position['capitalUtilisation'];
}): PriceCellProps['valueFormatted'] | undefined => { }): PriceCellProps['valueFormatted'] | undefined => {
if (!data) { if (!data || node?.rowPinned) {
return undefined; return undefined;
} }
return { return {
low: `${formatNumber(value, 2)}%`, low: `${formatNumber(value, 2)}%`,
high: addDecimalsFormatNumber( high: addDecimalsFormatNumber(data.totalBalance, data.decimals),
data.totalBalance,
data.assetDecimals
),
value: Number(value), value: Number(value),
}; };
}} }}
@ -271,18 +336,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
headerName={t('Realised PNL')} headerName={t('Realised PNL')}
field="realisedPNL" field="realisedPNL"
type="rightAligned" type="rightAligned"
cellClassRules={{ cellClassRules={signedNumberCssClassRules}
'text-vega-green-dark dark:text-vega-green': ({
value,
}: {
value: string;
}) => value && BigInt(value) > 0,
'text-vega-red-dark dark:text-vega-red': ({
value,
}: {
value: string;
}) => value && BigInt(value) < 0,
}}
valueFormatter={({ valueFormatter={({
value, value,
data, data,
@ -291,7 +345,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
}) => }) =>
value === undefined value === undefined
? undefined ? undefined
: addDecimalsFormatNumber(value.toString(), data.assetDecimals) : addDecimalsFormatNumber(value.toString(), data.decimals)
} }
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
headerTooltip={t('P&L excludes any fees paid.')} headerTooltip={t('P&L excludes any fees paid.')}
@ -300,18 +354,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
headerName={t('Unrealised PNL')} headerName={t('Unrealised PNL')}
field="unrealisedPNL" field="unrealisedPNL"
type="rightAligned" type="rightAligned"
cellClassRules={{ cellClassRules={signedNumberCssClassRules}
'text-vega-green-dark dark:text-vega-green': ({
value,
}: {
value: string;
}) => value && BigInt(value) > 0,
'text-vega-red-dark dark:text-vega-red': ({
value,
}: {
value: string;
}) => value && BigInt(value) < 0,
}}
valueFormatter={({ valueFormatter={({
value, value,
data, data,
@ -320,7 +363,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
}) => }) =>
value === undefined value === undefined
? undefined ? undefined
: addDecimalsFormatNumber(value.toString(), data.assetDecimals) : addDecimalsFormatNumber(value.toString(), data.decimals)
} }
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
/> />
@ -339,8 +382,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
return getDateTimeFormat().format(new Date(value)); return getDateTimeFormat().format(new Date(value));
}} }}
/> />
{onClose ? (
<AgGridColumn
cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : ButtonCell,
};
}}
cellRendererParams={{ onClick: onClose }}
/>
) : null}
</AgGrid> </AgGrid>
); );
}); }
);
export default PositionsTable; export default PositionsTable;

View File

@ -0,0 +1,108 @@
import { useRef, useCallback, useMemo, memo } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
import type { AgGridReact } from 'ag-grid-react';
import filter from 'lodash/filter';
import PositionsTable from './positions-table';
import type { GetRowsParams } from './positions-table';
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
import { AssetBalance } from '@vegaprotocol/accounts';
import type { Position } from './positions-data-providers';
interface PositionsProps {
partyId: string;
assetSymbol: string;
onClose: (position: Position) => void;
}
const getSummaryRow = (positions: Position[]) => {
const summaryRow = {
notional: new BigNumber(0),
realisedPNL: BigInt(0),
unrealisedPNL: BigInt(0),
};
positions.forEach((position) => {
summaryRow.notional = summaryRow.notional.plus(
toBigNum(position.notional, position.marketDecimalPlaces)
);
summaryRow.realisedPNL += BigInt(position.realisedPNL);
summaryRow.unrealisedPNL += BigInt(position.unrealisedPNL);
});
const decimals = positions[0]?.decimals || 0;
return {
marketName: t('Total'),
// we are using asset decimals instead of market decimals because each market can have different decimals
notional: summaryRow.notional
.multipliedBy(10 ** decimals)
.toFixed()
.toString(),
realisedPNL: summaryRow.realisedPNL.toString(),
unrealisedPNL: summaryRow.unrealisedPNL.toString(),
decimals,
};
};
export const Positions = memo(
({ partyId, assetSymbol, onClose }: PositionsProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(
({ data }: { data: Position[] | null }) => {
if (!gridRef.current?.api) {
return false;
}
dataRef.current = filter(data, { assetSymbol });
gridRef.current.api.refreshInfiniteCache();
return true;
},
[assetSymbol]
);
const { data, error, loading } = useDataProvider<Position[], never>({
dataProvider,
update,
variables,
});
dataRef.current = filter(data, { assetSymbol });
const getRows = async ({
successCallback,
startRow,
endRow,
}: GetRowsParams) => {
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current?.length ?? -1;
successCallback(rowsThisBlock, lastRow);
if (gridRef.current?.api) {
gridRef.current.api.setPinnedBottomRowData([
getSummaryRow(rowsThisBlock),
]);
}
};
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<div className="p-2">
<h4 className="text-lg">
{assetSymbol} {t('markets')}
</h4>
<p>
{assetSymbol} {t('balance')}:
<span className="pl-1 font-mono">
<AssetBalance partyId={partyId} assetSymbol={assetSymbol} />
</span>
</p>
</div>
<PositionsTable
domLayout="autoHeight"
style={{ width: '100%' }}
ref={gridRef}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
onClose={onClose}
/>
</AsyncRenderer>
);
}
);

View File

@ -0,0 +1,64 @@
import { useCallback } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { determineId } from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
import * as Sentry from '@sentry/react';
import { usePositionEvent } from '../';
import type { Position } from '../';
export const useClosePosition = () => {
const { keypair } = useVegaWallet();
const waitForPositionEvent = usePositionEvent();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
TransactionDialog,
} = useVegaTransaction();
const reset = useCallback(() => {
resetTransaction();
}, [resetTransaction]);
const submit = useCallback(
async (position: Position) => {
if (!keypair || position.openVolume === '0') {
return;
}
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
marketId: position.marketId,
orderId: '',
},
});
if (res?.signature) {
const resId = determineId(res.signature);
if (resId) {
waitForPositionEvent(resId, keypair.pub, () => {
setComplete();
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[keypair, send, setComplete, waitForPositionEvent]
);
return {
transaction,
TransactionDialog,
submit,
reset,
};
};

View File

@ -0,0 +1,14 @@
import { useCallback } from 'react';
// this should be replaced by implementation of busEvents listener when it will be available
export const usePositionEvent = () => {
const waitForOrderEvent = useCallback(
(id: string, partyId: string, callback: () => void) => {
Promise.resolve().then(() => {
callback();
});
},
[]
);
return waitForOrderEvent;
};

View File

@ -488,23 +488,27 @@ function makeDerivedDataProviderInternal<Data>(
let loaded = false; let loaded = false;
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext // notify single callback about current state, delta is passes optionally only if notify was invoked onNext
const notify = (callback: UpdateCallback<Data, never>) => { const notify = (
callback: UpdateCallback<Data, never>,
updateData?: UpdateData<Data, never>
) => {
callback({ callback({
data, data,
error, error,
loading, loading,
loaded, loaded,
pageInfo: null, pageInfo: null,
...updateData,
}); });
}; };
// notify all callbacks // notify all callbacks
const notifyAll = () => const notifyAll = (updateData?: UpdateData<Data, never>) =>
callbacks.forEach((callback) => { callbacks.forEach((callback) => {
notify(callback); notify(callback, updateData);
}); });
const combine = () => { const combine = (isUpdate = false) => {
let newError: Error | undefined; let newError: Error | undefined;
let newLoading = false; let newLoading = false;
let newLoaded = true; let newLoaded = true;
@ -529,7 +533,7 @@ function makeDerivedDataProviderInternal<Data>(
error = newError; error = newError;
loaded = newLoaded; loaded = newLoaded;
data = newData; data = newData;
notifyAll(); notifyAll({ isUpdate });
} }
}; };
@ -541,7 +545,7 @@ function makeDerivedDataProviderInternal<Data>(
dependency( dependency(
(updateData) => { (updateData) => {
parts[i] = updateData; parts[i] = updateData;
combine(); combine(updateData.isUpdate);
}, },
client, client,
variables variables

View File

@ -0,0 +1,23 @@
export const positiveClassNames = 'text-vega-green-dark dark:text-vega-green';
export const negativeClassNames = 'text-vega-red-dark dark:text-vega-red';
const isPositive = ({ value }: { value: string | bigint | number }) =>
value && ((typeof value === 'string' && !value.startsWith('-')) || value > 0);
const isNegative = ({ value }: { value: string | bigint | number }) =>
value && ((typeof value === 'string' && value.startsWith('-')) || value < 0);
export const signedNumberCssClass = (value: string | bigint | number) => {
if (isPositive({ value })) {
return positiveClassNames;
}
if (isNegative({ value })) {
return negativeClassNames;
}
return '';
};
export const signedNumberCssClassRules = {
[positiveClassNames]: isPositive,
[negativeClassNames]: isNegative,
};

View File

@ -1,3 +1,4 @@
export * from './cell-class-rules';
export * from './cumulative-vol-cell'; export * from './cumulative-vol-cell';
export * from './flash-cell'; export * from './flash-cell';
export * from './price-cell'; export * from './price-cell';

View File

@ -4,7 +4,7 @@ import {
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import { signedNumberCssClass } from '@vegaprotocol/react-helpers';
import { Arrow } from '../arrows/arrow'; import { Arrow } from '../arrows/arrow';
export interface PriceChangeCellProps { export interface PriceChangeCellProps {
@ -36,20 +36,13 @@ export const priceChange = (candles: string[]) => {
: 0; : 0;
}; };
const priceChangeClassNames = (value: number | bigint) =>
value === 0
? 'text-black dark:text-white'
: value > 0
? `text-vega-green-dark dark:text-vega-green `
: `text-vega-red-dark dark:text-vega-red`;
export const PriceCellChange = React.memo( export const PriceCellChange = React.memo(
({ candles, decimalPlaces }: PriceChangeCellProps) => { ({ candles, decimalPlaces }: PriceChangeCellProps) => {
const change = priceChange(candles); const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles); const changePercentage = priceChangePercentage(candles);
return ( return (
<span <span
className={`${priceChangeClassNames( className={`${signedNumberCssClass(
change change
)} flex items-center gap-2 justify-end`} )} flex items-center gap-2 justify-end`}
> >