* 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 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 = [
|
||||||
|
@ -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));
|
||||||
|
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-data-provider';
|
||||||
export * from './accounts-manager';
|
export * from './accounts-manager';
|
||||||
export * from './accounts-table';
|
export * from './accounts-table';
|
||||||
|
export * from './asset-balance';
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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} />
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
@ -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)}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{`${
|
{`${
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 { 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,
|
||||||
|
@ -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';
|
||||||
|
@ -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');
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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;
|
||||||
|
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;
|
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
|
||||||
|
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 './cumulative-vol-cell';
|
||||||
export * from './flash-cell';
|
export * from './flash-cell';
|
||||||
export * from './price-cell';
|
export * from './price-cell';
|
||||||
|
@ -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`}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user