* 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:
parent
fe716f74a1
commit
6de90c6b1f
@ -5,19 +5,22 @@ import { Stepper } from '../stepper';
|
||||
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
|
||||
import { InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import {
|
||||
getOrderDialogTitle,
|
||||
getOrderDialogIntent,
|
||||
getOrderDialogIcon,
|
||||
MarketSelector,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
import { MarketSelector } from '@vegaprotocol/deal-ticket';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
t,
|
||||
addDecimal,
|
||||
toDecimal,
|
||||
removeDecimal,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
getDefaultOrder,
|
||||
useOrderValidation,
|
||||
useOrderSubmit,
|
||||
getOrderDialogTitle,
|
||||
getOrderDialogIntent,
|
||||
getOrderDialogIcon,
|
||||
OrderFeedback,
|
||||
validateSize,
|
||||
} from '@vegaprotocol/orders';
|
||||
@ -108,7 +111,7 @@ export const DealTicketSteps = ({
|
||||
});
|
||||
|
||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
||||
useOrderSubmit(market);
|
||||
useOrderSubmit();
|
||||
|
||||
const onSizeChange = (value: number[]) => {
|
||||
const newVal = new BigNumber(value[0])
|
||||
@ -151,10 +154,20 @@ export const DealTicketSteps = ({
|
||||
const onSubmit = React.useCallback(
|
||||
(order: Order) => {
|
||||
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 = [
|
||||
|
@ -25,7 +25,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
|
||||
({ delta }: { delta: AccountSubscribe_accounts }) => {
|
||||
const update: Accounts_party_accounts[] = [];
|
||||
const add: Accounts_party_accounts[] = [];
|
||||
if (!gridRef.current) {
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
const rowNode = gridRef.current.api.getRowNode(getId(delta));
|
||||
|
42
libs/accounts/src/lib/asset-balance.tsx
Normal file
42
libs/accounts/src/lib/asset-balance.tsx
Normal 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;
|
||||
};
|
@ -3,3 +3,4 @@ export * from './accounts-container';
|
||||
export * from './accounts-data-provider';
|
||||
export * from './accounts-manager';
|
||||
export * from './accounts-table';
|
||||
export * from './asset-balance';
|
||||
|
@ -17,7 +17,7 @@ export const DealTicketManager = ({
|
||||
children,
|
||||
}: DealTicketManagerProps) => {
|
||||
const { submit, transaction, finalizedOrder, TransactionDialog } =
|
||||
useOrderSubmit(market);
|
||||
useOrderSubmit();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
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 { TypeSelector } from './type-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
@ -51,10 +55,15 @@ export const DealTicket = ({
|
||||
const onSubmit = useCallback(
|
||||
(order: Order) => {
|
||||
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 (
|
||||
@ -111,7 +120,7 @@ export const DealTicket = ({
|
||||
{orderType === OrderType.TYPE_LIMIT &&
|
||||
orderTimeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT && (
|
||||
<Controller
|
||||
name="expiration"
|
||||
name="expiresAt"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<ExpirySelector value={field.value} onSelect={field.onChange} />
|
||||
|
@ -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 { Side } from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
@ -141,82 +141,78 @@ describe('FillsTable', () => {
|
||||
});
|
||||
|
||||
it('should format cells correctly for seller fill', async () => {
|
||||
act(async () => {
|
||||
const partyId = 'party-id';
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Side.SIDE_SELL,
|
||||
sellerFee: {
|
||||
makerFee: '1',
|
||||
infrastructureFee: '1',
|
||||
liquidityFee: '1',
|
||||
},
|
||||
});
|
||||
|
||||
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||
await waitForGridToBeInTheDOM();
|
||||
await waitForDataToHaveLoaded();
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
buyerFill.market.name,
|
||||
'-3.00000',
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
'Taker',
|
||||
'0.03 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
|
||||
const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(amountCell).toHaveClass('text-vega-red');
|
||||
const partyId = 'party-id';
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Side.SIDE_SELL,
|
||||
sellerFee: {
|
||||
makerFee: '1',
|
||||
infrastructureFee: '1',
|
||||
liquidityFee: '1',
|
||||
},
|
||||
});
|
||||
|
||||
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||
await waitForGridToBeInTheDOM();
|
||||
await waitForDataToHaveLoaded();
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
buyerFill.market.name,
|
||||
'-3.00000',
|
||||
'1.00 BTC',
|
||||
'3.00 BTC',
|
||||
'Taker',
|
||||
'0.03 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
];
|
||||
cells.forEach((cell, i) => {
|
||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||
});
|
||||
|
||||
const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(amountCell).toHaveClass('text-vega-red-dark');
|
||||
});
|
||||
|
||||
it('should render correct maker or taker role', async () => {
|
||||
act(async () => {
|
||||
const partyId = 'party-id';
|
||||
const takerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Side.SIDE_SELL,
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<FillsTable partyId={partyId} rowData={[takerFill]} />
|
||||
);
|
||||
await waitForGridToBeInTheDOM();
|
||||
await waitForDataToHaveLoaded();
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'aggressor')
|
||||
).toHaveTextContent('Taker');
|
||||
|
||||
const makerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Side.SIDE_BUY,
|
||||
});
|
||||
|
||||
rerender(<FillsTable partyId={partyId} rowData={[makerFill]} />);
|
||||
await waitForGridToBeInTheDOM();
|
||||
await waitForDataToHaveLoaded();
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'aggressor')
|
||||
).toHaveTextContent('Maker');
|
||||
const partyId = 'party-id';
|
||||
const takerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Side.SIDE_SELL,
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<FillsTable partyId={partyId} rowData={[takerFill]} />
|
||||
);
|
||||
await waitForGridToBeInTheDOM();
|
||||
await waitForDataToHaveLoaded();
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'aggressor')
|
||||
).toHaveTextContent('Taker');
|
||||
|
||||
const makerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Side.SIDE_BUY,
|
||||
});
|
||||
|
||||
rerender(<FillsTable partyId={partyId} rowData={[makerFill]} />);
|
||||
await waitForGridToBeInTheDOM();
|
||||
await waitForDataToHaveLoaded();
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'aggressor')
|
||||
).toHaveTextContent('Maker');
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,8 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
positiveClassNames,
|
||||
negativeClassNames,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
@ -49,9 +51,8 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
field="size"
|
||||
cellClass={({ data }: { data: FillFields }) => {
|
||||
return classNames('text-right', {
|
||||
'text-vega-green-dark dark:text-vega-green':
|
||||
data?.buyer.id === partyId,
|
||||
'text-vega-red-dark dark:text-vega-red': data?.seller.id,
|
||||
[positiveClassNames]: data?.buyer.id === partyId,
|
||||
[negativeClassNames]: data?.seller.id,
|
||||
});
|
||||
}}
|
||||
valueFormatter={formatSize(partyId)}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
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 {
|
||||
OrderRejectionReasonMapping,
|
||||
OrderStatus,
|
||||
@ -49,8 +54,8 @@ export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {
|
||||
<p
|
||||
className={
|
||||
order.side === Side.SIDE_BUY
|
||||
? 'text-vega-green-dark dark:text-vega-green'
|
||||
: 'text-vega-red-dark dark:text-vega-red'
|
||||
? positiveClassNames
|
||||
: negativeClassNames
|
||||
}
|
||||
>
|
||||
{`${
|
||||
|
@ -8,7 +8,13 @@ import {
|
||||
OrderTimeInForceMapping,
|
||||
OrderRejectionReasonMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { addDecimal, getDateTimeFormat, t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
addDecimal,
|
||||
getDateTimeFormat,
|
||||
t,
|
||||
positiveClassNames,
|
||||
negativeClassNames,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
Button,
|
||||
@ -125,12 +131,12 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
cellClass="font-mono text-right"
|
||||
type="rightAligned"
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
[positiveClassNames]: ({
|
||||
data,
|
||||
}: {
|
||||
data: Orders_party_ordersConnection_edges_node;
|
||||
}) => data?.side === Side.SIDE_BUY,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
[negativeClassNames]: ({
|
||||
data,
|
||||
}: {
|
||||
data: Orders_party_ordersConnection_edges_node;
|
||||
|
@ -60,10 +60,7 @@ const defaultWalletContext = {
|
||||
connector: null,
|
||||
};
|
||||
|
||||
function setup(
|
||||
context?: Partial<VegaWalletContextShape>,
|
||||
market = defaultMarket
|
||||
) {
|
||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
||||
const mocks: MockedResponse<OrderEvent> = {
|
||||
request: {
|
||||
query: ORDER_EVENT_SUB,
|
||||
@ -144,7 +141,7 @@ function setup(
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useOrderSubmit(market), { wrapper });
|
||||
return renderHook(() => useOrderSubmit(), { wrapper });
|
||||
}
|
||||
|
||||
describe('useOrderSubmit', () => {
|
||||
@ -164,11 +161,11 @@ describe('useOrderSubmit', () => {
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
side: Side.SIDE_BUY,
|
||||
price: '1234567.89',
|
||||
expiration: new Date('2022-01-01'),
|
||||
price: '123456789',
|
||||
expiresAt: new Date('2022-01-01'),
|
||||
};
|
||||
await act(async () => {
|
||||
result.current.submit(order);
|
||||
result.current.submit({ ...order, marketId: defaultMarket.id });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith({
|
||||
@ -176,14 +173,12 @@ describe('useOrderSubmit', () => {
|
||||
propagate: true,
|
||||
orderSubmission: {
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
marketId: defaultMarket.id, // Market provided from hook argument
|
||||
size: '100', // size adjusted based on positionDecimalPlaces
|
||||
marketId: defaultMarket.id,
|
||||
size: '10',
|
||||
side: Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
price: '123456789', // Decimal removed
|
||||
expiresAt: order.expiration
|
||||
? toNanoSeconds(order.expiration)
|
||||
: undefined,
|
||||
price: '123456789',
|
||||
expiresAt: order.expiresAt ? toNanoSeconds(order.expiresAt) : undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
165
libs/orders/src/lib/order-hooks/use-order-submit.tsx
Normal file
165
libs/orders/src/lib/order-hooks/use-order-submit.tsx
Normal 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,
|
||||
};
|
||||
};
|
@ -1,8 +1,12 @@
|
||||
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';
|
||||
|
||||
export const getDefaultOrder = (market: Market): Order => ({
|
||||
export const getDefaultOrder = (market: {
|
||||
id: string;
|
||||
positionDecimalPlaces: number;
|
||||
}): Order => ({
|
||||
marketId: market.id,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
side: Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
|
@ -1,4 +1,6 @@
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/__generated__/Positions';
|
||||
export * from './lib/positions-container';
|
||||
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';
|
||||
|
@ -197,7 +197,7 @@ describe('getMetrics', () => {
|
||||
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].liquidationPrice).toEqual('169990');
|
||||
expect(metrics[0].lowMarginLevel).toEqual(false);
|
||||
expect(metrics[0].markPrice).toEqual('9431775');
|
||||
@ -222,7 +222,7 @@ describe('getMetrics', () => {
|
||||
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].liquidationPrice).toEqual('9830750');
|
||||
expect(metrics[1].lowMarginLevel).toEqual(false);
|
||||
expect(metrics[1].markPrice).toEqual('869762');
|
||||
|
@ -24,7 +24,7 @@ export interface Position {
|
||||
averageEntryPrice: string;
|
||||
capitalUtilisation: number;
|
||||
currentLeverage: number;
|
||||
assetDecimals: number;
|
||||
decimals: number;
|
||||
marketDecimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
totalBalance: string;
|
||||
@ -133,7 +133,12 @@ export const getMetrics = (
|
||||
const marginAccount = accounts?.find((account) => {
|
||||
return account.market?.id === market.id;
|
||||
});
|
||||
if (!marginAccount || !marginLevel || !marketData) {
|
||||
if (
|
||||
!marginAccount ||
|
||||
!marginLevel ||
|
||||
!marketData ||
|
||||
position.node.openVolume === '0'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const generalAccount = accounts?.find(
|
||||
@ -141,7 +146,7 @@ export const getMetrics = (
|
||||
account.asset.id === marginAccount.asset.id &&
|
||||
account.type === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
const assetDecimals = marginAccount.asset.decimals;
|
||||
const decimals = marginAccount.asset.decimals;
|
||||
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
|
||||
market;
|
||||
const openVolume = toBigNum(
|
||||
@ -149,13 +154,10 @@ export const getMetrics = (
|
||||
positionDecimalPlaces
|
||||
);
|
||||
|
||||
const marginAccountBalance = toBigNum(
|
||||
marginAccount.balance ?? 0,
|
||||
assetDecimals
|
||||
);
|
||||
const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals);
|
||||
const generalAccountBalance = toBigNum(
|
||||
generalAccount?.balance ?? 0,
|
||||
assetDecimals
|
||||
decimals
|
||||
);
|
||||
const markPrice = toBigNum(marketData.markPrice, marketDecimalPlaces);
|
||||
|
||||
@ -180,19 +182,19 @@ export const getMetrics = (
|
||||
marketDecimalPlaces
|
||||
);
|
||||
|
||||
const searchPrice = openVolume.isEqualTo(0)
|
||||
? markPrice
|
||||
: marginSearch
|
||||
.minus(marginAccountBalance)
|
||||
.dividedBy(openVolume)
|
||||
.plus(markPrice);
|
||||
const liquidationPrice = openVolume.isEqualTo(0)
|
||||
? markPrice
|
||||
: marginMaintenance
|
||||
.minus(marginAccountBalance)
|
||||
.minus(generalAccountBalance)
|
||||
.dividedBy(openVolume)
|
||||
.plus(markPrice);
|
||||
const searchPrice = marginSearch
|
||||
.minus(marginAccountBalance)
|
||||
.dividedBy(openVolume)
|
||||
.plus(markPrice);
|
||||
|
||||
const liquidationPrice = BigNumber.maximum(
|
||||
0,
|
||||
marginMaintenance
|
||||
.minus(marginAccountBalance)
|
||||
.minus(generalAccountBalance)
|
||||
.dividedBy(openVolume)
|
||||
.plus(markPrice)
|
||||
);
|
||||
|
||||
const lowMarginLevel =
|
||||
marginAccountBalance.isLessThan(
|
||||
@ -206,9 +208,9 @@ export const getMetrics = (
|
||||
currentLeverage: currentLeverage.toNumber(),
|
||||
marketDecimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
assetDecimals,
|
||||
decimals,
|
||||
assetSymbol: marginLevel.asset.symbol,
|
||||
totalBalance: totalBalance.multipliedBy(10 ** assetDecimals).toFixed(),
|
||||
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
|
||||
lowMarginLevel,
|
||||
liquidationPrice: liquidationPrice
|
||||
.multipliedBy(10 ** marketDecimalPlaces)
|
||||
|
@ -1,26 +1,40 @@
|
||||
import { useRef, useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
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 type { Position } from './positions-data-providers';
|
||||
import { Positions } from './positions';
|
||||
import { useClosePosition } from '../';
|
||||
|
||||
interface PositionsManagerProps {
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
const getSymbols = (positions: Position[]) =>
|
||||
Array.from(new Set(positions.map((position) => position.assetSymbol))).sort();
|
||||
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
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 }) => {
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
if (data?.length) {
|
||||
const newAssetSymbols = getSymbols(data);
|
||||
if (
|
||||
!newAssetSymbols.every(
|
||||
(symbol) =>
|
||||
assetSymbols.current && assetSymbols.current.includes(symbol)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
dataRef.current = data;
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
return true;
|
||||
}, []);
|
||||
const { data, error, loading } = useDataProvider<Position[], never>({
|
||||
@ -28,26 +42,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
update,
|
||||
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 (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
<PositionsTable
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.length ? undefined : []}
|
||||
datasource={{ getRows }}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
<>
|
||||
<AsyncRenderer loading={loading} error={error} data={assetSymbols}>
|
||||
{data &&
|
||||
getSymbols(data)?.map((assetSymbol) => (
|
||||
<Positions
|
||||
partyId={partyId}
|
||||
assetSymbol={assetSymbol}
|
||||
key={assetSymbol}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
</AsyncRenderer>
|
||||
<TransactionDialog>
|
||||
<p>Your position was not closed! This is still not implemented. </p>
|
||||
</TransactionDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -11,7 +11,7 @@ const singleRow: Position = {
|
||||
currentLeverage: 1.1,
|
||||
marketDecimalPlaces: 1,
|
||||
positionDecimalPlaces: 0,
|
||||
assetDecimals: 2,
|
||||
decimals: 2,
|
||||
totalBalance: '123456',
|
||||
assetSymbol: 'BTC',
|
||||
liquidationPrice: '83', // 8.3
|
||||
|
@ -16,7 +16,7 @@ const longPosition: Position = {
|
||||
averageEntryPrice: '1134564',
|
||||
capitalUtilisation: 10,
|
||||
currentLeverage: 11,
|
||||
assetDecimals: 2,
|
||||
decimals: 2,
|
||||
marketDecimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
// generalAccountBalance: '0',
|
||||
@ -48,7 +48,7 @@ const shortPosition: Position = {
|
||||
averageEntryPrice: '23976',
|
||||
capitalUtilisation: 87,
|
||||
currentLeverage: 7,
|
||||
assetDecimals: 2,
|
||||
decimals: 2,
|
||||
marketDecimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
// generalAccountBalance: '0',
|
||||
|
@ -1,6 +1,12 @@
|
||||
import classNames from 'classnames';
|
||||
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 {
|
||||
PriceFlashCell,
|
||||
addDecimalsFormatNumber,
|
||||
@ -8,6 +14,8 @@ import {
|
||||
t,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
signedNumberCssClass,
|
||||
signedNumberCssClassRules,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
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 { Position } from './positions-data-providers';
|
||||
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;
|
||||
|
||||
@ -29,6 +37,8 @@ export interface Datasource extends IDatasource {
|
||||
interface Props extends AgGridReactProps {
|
||||
rowData?: Position[] | null;
|
||||
datasource?: Datasource;
|
||||
onClose?: (data: Position) => void;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
type PositionsTableValueFormatterParams = Omit<
|
||||
@ -43,12 +53,15 @@ export interface MarketNameCellProps {
|
||||
}
|
||||
|
||||
export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
|
||||
return valueFormatted ? (
|
||||
<div className="leading-tight">
|
||||
<div>{valueFormatted[0]}</div>
|
||||
{valueFormatted[1] ? <div>{valueFormatted[1]}</div> : null}
|
||||
</div>
|
||||
) : null;
|
||||
if (valueFormatted && valueFormatted[1]) {
|
||||
return (
|
||||
<div className="leading-tight">
|
||||
<div>{valueFormatted[0]}</div>
|
||||
<div>{valueFormatted[1]}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (valueFormatted && valueFormatted[0]) || undefined;
|
||||
};
|
||||
|
||||
export interface PriceCellProps {
|
||||
@ -63,14 +76,14 @@ export interface PriceCellProps {
|
||||
export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
|
||||
return valueFormatted ? (
|
||||
<>
|
||||
<div className="flex justify-between leading-tight">
|
||||
<div className="flex justify-between leading-tight font-mono">
|
||||
<div>{valueFormatted.low}</div>
|
||||
<div>{valueFormatted.high}</div>
|
||||
</div>
|
||||
<ProgressBar
|
||||
value={valueFormatted.value}
|
||||
intent={valueFormatted.intent}
|
||||
className="mt-4"
|
||||
className="mt-2"
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
@ -91,14 +104,10 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
|
||||
}
|
||||
const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } =
|
||||
valueFormatted;
|
||||
const isShortPosition = openVolume.startsWith('-');
|
||||
return valueFormatted ? (
|
||||
<div className="leading-tight">
|
||||
<div className="leading-tight font-mono">
|
||||
<div
|
||||
className={classNames('text-right', {
|
||||
'text-vega-green-dark dark:text-vega-green': !isShortPosition,
|
||||
'text-vega-red-dark dark:text-vega-red': isShortPosition,
|
||||
})}
|
||||
className={classNames('text-right', signedNumberCssClass(openVolume))}
|
||||
>
|
||||
{volumePrefix(
|
||||
addDecimalsFormatNumber(openVolume, positionDecimalPlaces)
|
||||
@ -113,234 +122,281 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
|
||||
|
||||
AmountCell.displayName = 'AmountCell';
|
||||
|
||||
export const PositionsTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
const ButtonCell = ({
|
||||
onClick,
|
||||
data,
|
||||
}: {
|
||||
onClick: (position: Position) => void;
|
||||
data: Position;
|
||||
}) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate="No positions"
|
||||
getRowId={getRowId}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
}}
|
||||
components={{ PriceFlashCell, ProgressBarCell }}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketName"
|
||||
cellRenderer={MarketNameCell}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['marketName'];
|
||||
}) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
// split market name into two parts, 'Part1 (Part2)'
|
||||
const matches = value.match(/^(.*)\((.*)\)\s*$/);
|
||||
if (matches) {
|
||||
return [matches[1].trim(), matches[2].trim()];
|
||||
}
|
||||
return [value];
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Amount')}
|
||||
field="openVolume"
|
||||
type="rightAligned"
|
||||
cellRenderer={AmountCell}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['openVolume'];
|
||||
}): AmountCellProps['valueFormatted'] => {
|
||||
if (!value || !data) {
|
||||
return undefined;
|
||||
}
|
||||
return data;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark price')}
|
||||
field="markPrice"
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['markPrice'];
|
||||
}) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
data.marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
value.toString(),
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Entry price')}
|
||||
field="averageEntryPrice"
|
||||
headerComponentParams={{
|
||||
template:
|
||||
'<div class="ag-cell-label-container" role="presentation">' +
|
||||
` <span>${t('Liquidation price (est)')}</span>` +
|
||||
' <span ref="eText" class="ag-header-cell-text"></span>' +
|
||||
'</div>',
|
||||
}}
|
||||
flex={2}
|
||||
cellRenderer="ProgressBarCell"
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams):
|
||||
| PriceCellProps['valueFormatted']
|
||||
| undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.averageEntryPrice);
|
||||
const max = BigInt(data.liquidationPrice);
|
||||
const mid = BigInt(data.markPrice);
|
||||
const range = max - min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(
|
||||
min.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
high: addDecimalsFormatNumber(
|
||||
max.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
|
||||
intent: data.lowMarginLevel ? Intent.Warning : undefined,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Leverage')}
|
||||
field="currentLeverage"
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['currentLeverage'];
|
||||
}) =>
|
||||
value === undefined ? undefined : formatNumber(value.toString(), 1)
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Margin allocated')}
|
||||
field="capitalUtilisation"
|
||||
type="rightAligned"
|
||||
flex={2}
|
||||
cellRenderer="ProgressBarCell"
|
||||
valueFormatter={({
|
||||
data,
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['capitalUtilisation'];
|
||||
}): PriceCellProps['valueFormatted'] | undefined => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
low: `${formatNumber(value, 2)}%`,
|
||||
high: addDecimalsFormatNumber(
|
||||
data.totalBalance,
|
||||
data.assetDecimals
|
||||
),
|
||||
value: Number(value),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Realised PNL')}
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => value && BigInt(value) > 0,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => value && BigInt(value) < 0,
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['realisedPNL'];
|
||||
}) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.assetDecimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
headerTooltip={t('P&L excludes any fees paid.')}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={{
|
||||
'text-vega-green-dark dark:text-vega-green': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => value && BigInt(value) > 0,
|
||||
'text-vega-red-dark dark:text-vega-red': ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => value && BigInt(value) < 0,
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['unrealisedPNL'];
|
||||
}) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.assetDecimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
field="updatedAt"
|
||||
type="rightAligned"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['updatedAt'];
|
||||
}) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
<Button onClick={() => onClick(data)} size="sm">
|
||||
{t('Close')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const EmptyCell = () => '';
|
||||
|
||||
export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
({ onClose, ...props }, ref) => {
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate="No positions"
|
||||
getRowId={getRowId}
|
||||
rowHeight={34}
|
||||
ref={ref}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
}}
|
||||
components={{ PriceFlashCell, ProgressBarCell }}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="marketName"
|
||||
cellRenderer={MarketNameCell}
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['marketName'];
|
||||
}) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
// split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2'
|
||||
const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/);
|
||||
if (matches) {
|
||||
return [matches[1].trim(), matches[3].trim()];
|
||||
}
|
||||
return [value];
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Amount')}
|
||||
field="openVolume"
|
||||
valueGetter={({ node, data }: ValueGetterParams) => {
|
||||
return node?.rowPinned ? data?.notional : data?.openVolume;
|
||||
}}
|
||||
type="rightAligned"
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? PriceFlashCell : AmountCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['openVolume'];
|
||||
}): AmountCellProps['valueFormatted'] | string => {
|
||||
if (!value || !data) {
|
||||
return undefined;
|
||||
}
|
||||
if (node?.rowPinned) {
|
||||
return addDecimalsFormatNumber(value, data.decimals);
|
||||
}
|
||||
return data;
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark price')}
|
||||
field="markPrice"
|
||||
type="rightAligned"
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['markPrice'];
|
||||
}) => {
|
||||
if (!data || !value || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
data.marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
value.toString(),
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Entry price')}
|
||||
field="averageEntryPrice"
|
||||
headerComponentParams={{
|
||||
template:
|
||||
'<div class="ag-cell-label-container" role="presentation">' +
|
||||
` <span>${t('Liquidation price (est)')}</span>` +
|
||||
' <span ref="eText" class="ag-header-cell-text"></span>' +
|
||||
'</div>',
|
||||
}}
|
||||
flex={2}
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams):
|
||||
| PriceCellProps['valueFormatted']
|
||||
| undefined => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
const min = BigInt(data.averageEntryPrice);
|
||||
const max = BigInt(data.liquidationPrice);
|
||||
const mid = BigInt(data.markPrice);
|
||||
const range = max - min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(
|
||||
min.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
high: addDecimalsFormatNumber(
|
||||
max.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
value: range ? Number(((mid - min) * BigInt(100)) / range) : 0,
|
||||
intent: data.lowMarginLevel ? Intent.Warning : undefined,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Leverage')}
|
||||
field="currentLeverage"
|
||||
type="rightAligned"
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['currentLeverage'];
|
||||
}) =>
|
||||
value === undefined ? undefined : formatNumber(value.toString(), 1)
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Margin allocated')}
|
||||
field="capitalUtilisation"
|
||||
type="rightAligned"
|
||||
flex={2}
|
||||
cellRenderer="ProgressBarCell"
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
|
||||
};
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
value,
|
||||
node,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['capitalUtilisation'];
|
||||
}): PriceCellProps['valueFormatted'] | undefined => {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
low: `${formatNumber(value, 2)}%`,
|
||||
high: addDecimalsFormatNumber(data.totalBalance, data.decimals),
|
||||
value: Number(value),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Realised PNL')}
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['realisedPNL'];
|
||||
}) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
headerTooltip={t('P&L excludes any fees paid.')}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Unrealised PNL')}
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['unrealisedPNL'];
|
||||
}) =>
|
||||
value === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Updated')}
|
||||
field="updatedAt"
|
||||
type="rightAligned"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: PositionsTableValueFormatterParams & {
|
||||
value: Position['updatedAt'];
|
||||
}) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
}}
|
||||
/>
|
||||
{onClose ? (
|
||||
<AgGridColumn
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : ButtonCell,
|
||||
};
|
||||
}}
|
||||
cellRendererParams={{ onClick: onClose }}
|
||||
/>
|
||||
) : null}
|
||||
</AgGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default PositionsTable;
|
||||
|
108
libs/positions/src/lib/positions.tsx
Normal file
108
libs/positions/src/lib/positions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
64
libs/positions/src/lib/use-close-position.ts
Normal file
64
libs/positions/src/lib/use-close-position.ts
Normal 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,
|
||||
};
|
||||
};
|
14
libs/positions/src/lib/use-position-event.ts
Normal file
14
libs/positions/src/lib/use-position-event.ts
Normal 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;
|
||||
};
|
@ -488,23 +488,27 @@ function makeDerivedDataProviderInternal<Data>(
|
||||
let loaded = false;
|
||||
|
||||
// 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({
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
loaded,
|
||||
pageInfo: null,
|
||||
...updateData,
|
||||
});
|
||||
};
|
||||
|
||||
// notify all callbacks
|
||||
const notifyAll = () =>
|
||||
const notifyAll = (updateData?: UpdateData<Data, never>) =>
|
||||
callbacks.forEach((callback) => {
|
||||
notify(callback);
|
||||
notify(callback, updateData);
|
||||
});
|
||||
|
||||
const combine = () => {
|
||||
const combine = (isUpdate = false) => {
|
||||
let newError: Error | undefined;
|
||||
let newLoading = false;
|
||||
let newLoaded = true;
|
||||
@ -529,7 +533,7 @@ function makeDerivedDataProviderInternal<Data>(
|
||||
error = newError;
|
||||
loaded = newLoaded;
|
||||
data = newData;
|
||||
notifyAll();
|
||||
notifyAll({ isUpdate });
|
||||
}
|
||||
};
|
||||
|
||||
@ -541,7 +545,7 @@ function makeDerivedDataProviderInternal<Data>(
|
||||
dependency(
|
||||
(updateData) => {
|
||||
parts[i] = updateData;
|
||||
combine();
|
||||
combine(updateData.isUpdate);
|
||||
},
|
||||
client,
|
||||
variables
|
||||
|
23
libs/react-helpers/src/lib/grid/cell-class-rules.ts
Normal file
23
libs/react-helpers/src/lib/grid/cell-class-rules.ts
Normal 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,
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from './cell-class-rules';
|
||||
export * from './cumulative-vol-cell';
|
||||
export * from './flash-cell';
|
||||
export * from './price-cell';
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import { signedNumberCssClass } from '@vegaprotocol/react-helpers';
|
||||
import { Arrow } from '../arrows/arrow';
|
||||
|
||||
export interface PriceChangeCellProps {
|
||||
@ -36,20 +36,13 @@ export const priceChange = (candles: string[]) => {
|
||||
: 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(
|
||||
({ candles, decimalPlaces }: PriceChangeCellProps) => {
|
||||
const change = priceChange(candles);
|
||||
const changePercentage = priceChangePercentage(candles);
|
||||
return (
|
||||
<span
|
||||
className={`${priceChangeClassNames(
|
||||
className={`${signedNumberCssClass(
|
||||
change
|
||||
)} flex items-center gap-2 justify-end`}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user