feat(deal-ticket): show total margin available for an order, and where it will come from (#3318)

This commit is contained in:
Bartłomiej Głownia 2023-04-03 15:17:23 +02:00 committed by GitHub
parent 8463d371ad
commit 2aad6b1a14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 47 deletions

View File

@ -1,4 +1,5 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { Market, MarketData } from '@vegaprotocol/market-list'; import type { Market, MarketData } from '@vegaprotocol/market-list';
@ -11,9 +12,12 @@ interface DealTicketFeeDetailsProps {
order: OrderSubmissionBody['orderSubmission']; order: OrderSubmissionBody['orderSubmission'];
market: Market; market: Market;
marketData: MarketData; marketData: MarketData;
margin: string; currentInitialMargin?: string;
totalMargin: string; currentMaintenanceMargin?: string;
balance: string; estimatedInitialMargin: string;
estimatedTotalInitialMargin: string;
marginAccountBalance: string;
generalAccountBalance: string;
} }
export interface DealTicketFeeDetailProps { export interface DealTicketFeeDetailProps {
@ -45,23 +49,22 @@ export const DealTicketFeeDetails = ({
order, order,
market, market,
marketData, marketData,
margin, ...args
totalMargin,
balance,
}: DealTicketFeeDetailsProps) => { }: DealTicketFeeDetailsProps) => {
const feeDetails = useFeeDealTicketDetails(order, market, marketData); const feeDetails = useFeeDealTicketDetails(order, market, marketData);
const details = getFeeDetailsValues({ const details = getFeeDetailsValues({
...feeDetails, ...feeDetails,
margin, ...args,
totalMargin,
balance,
}); });
return ( return (
<div> <div>
{details.map(({ label, value, labelDescription, symbol }) => ( {details.map(({ label, value, labelDescription, symbol, indent }) => (
<div <div
key={typeof label === 'string' ? label : 'value-dropdown'} key={typeof label === 'string' ? label : 'value-dropdown'}
className="text-xs mt-2 flex justify-between items-center gap-4 flex-wrap" className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
> >
<div> <div>
<Tooltip description={labelDescription}> <Tooltip description={labelDescription}>

View File

@ -44,6 +44,9 @@ import {
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types'; import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import { useOrderForm } from '../../hooks/use-order-form'; import { useOrderForm } from '../../hooks/use-order-form';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { marketMarginDataProvider } from '@vegaprotocol/positions';
export interface DealTicketProps { export interface DealTicketProps {
market: Market; market: Market;
@ -103,6 +106,12 @@ export const DealTicket = ({
const { margin, totalMargin } = useInitialMargin(market.id, normalizedOrder); const { margin, totalMargin } = useInitialMargin(market.id, normalizedOrder);
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: pubKey || '' },
skip: !pubKey,
});
useEffect(() => { useEffect(() => {
if (!pubKey) { if (!pubKey) {
setError('summary', { setError('summary', {
@ -367,9 +376,12 @@ export const DealTicket = ({
order={normalizedOrder} order={normalizedOrder}
market={market} market={market}
marketData={marketData} marketData={marketData}
margin={margin} estimatedInitialMargin={margin}
totalMargin={totalMargin} estimatedTotalInitialMargin={totalMargin}
balance={marginAccountBalance} currentInitialMargin={currentMargins?.initialLevel}
currentMaintenanceMargin={currentMargins?.maintenanceLevel}
marginAccountBalance={marginAccountBalance}
generalAccountBalance={generalAccountBalance}
/> />
</form> </form>
</TinyScroll> </TinyScroll>

View File

@ -10,12 +10,36 @@ export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t( export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
'Estimated total margin that will cover open position, active orders and this order.' 'Estimated total margin that will cover open position, active orders and this order.'
); );
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance'); export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.');
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) => export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
t( t(
"The additional margin required for your new position (taking into account volume and open orders), compared to your current margin. Measured in the market's settlement asset (%s).", "The additional margin required for your new position (taking into account volume and open orders), compared to your current margin. Measured in the market's settlement asset (%s).",
[settlementAsset] [settlementAsset]
); );
export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT = (
settlementAsset: string
) =>
t(
'To cover the required margin, this amount will be drawn from your general (%s) account.',
[settlementAsset]
);
export const TOTAL_MARGIN_AVAILABLE = (
generalAccountBalance: string,
marginAccountBalance: string,
marginMaintenance: string,
settlementAsset: string
) =>
t(
'Total margin available = general %s balance (%s) + margin balance (%s) - maintenance level (%s).',
[
settlementAsset,
`${generalAccountBalance} ${settlementAsset}`,
`${marginAccountBalance} ${settlementAsset}`,
`${marginMaintenance} ${settlementAsset}`,
]
);
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t( export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.' 'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'
); );
@ -40,7 +64,7 @@ export const EST_SLIPPAGE = t(
); );
export const ERROR_SIZE_DECIMAL = t( export const ERROR_SIZE_DECIMAL = t(
'The size field accepts up to X decimal places' 'The size field accepts up to X decimal places.'
); );
export enum MarketModeValidationType { export enum MarketModeValidationType {

View File

@ -15,6 +15,8 @@ import {
NOTIONAL_SIZE_TOOLTIP_TEXT, NOTIONAL_SIZE_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT, MARGIN_ACCOUNT_TOOLTIP_TEXT,
MARGIN_DIFF_TOOLTIP_TEXT, MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
} from '../constants'; } from '../constants';
import { useOrderCloseOut } from './use-order-closeout'; import { useOrderCloseOut } from './use-order-closeout';
import { useMarketAccountBalance } from '@vegaprotocol/accounts'; import { useMarketAccountBalance } from '@vegaprotocol/accounts';
@ -85,24 +87,32 @@ export const useFeeDealTicketDetails = (
}; };
export interface FeeDetails { export interface FeeDetails {
balance: string; generalAccountBalance?: string;
marginAccountBalance?: string;
market: Market; market: Market;
assetSymbol: string; assetSymbol: string;
notionalSize: string | null; notionalSize: string | null;
estCloseOut: string | null; estCloseOut: string | null;
estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined; estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined;
margin: string; estimatedInitialMargin: string;
totalMargin: string; estimatedTotalInitialMargin: string;
currentInitialMargin?: string;
currentMaintenanceMargin?: string;
} }
export const getFeeDetailsValues = ({ export const getFeeDetailsValues = ({
balance, marginAccountBalance,
generalAccountBalance,
assetSymbol, assetSymbol,
estimateOrder, estimateOrder,
market, market,
notionalSize, notionalSize,
totalMargin, estimatedTotalInitialMargin,
currentInitialMargin,
currentMaintenanceMargin,
}: FeeDetails) => { }: FeeDetails) => {
const totalBalance =
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
const assetDecimals = const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals; market.tradableInstrument.instrument.product.settlementAsset.decimals;
const formatValueWithMarketDp = ( const formatValueWithMarketDp = (
@ -123,7 +133,8 @@ export const getFeeDetailsValues = ({
label: string; label: string;
value?: string | null; value?: string | null;
symbol: string; symbol: string;
labelDescription: React.ReactNode; indent?: boolean;
labelDescription?: React.ReactNode;
}[] = [ }[] = [
{ {
label: t('Notional'), label: t('Notional'),
@ -153,38 +164,64 @@ export const getFeeDetailsValues = ({
), ),
symbol: assetSymbol, symbol: assetSymbol,
}, },
/*
{
label: t('Initial margin'),
value: margin && `~${formatValueWithAssetDp(margin)}`,
symbol: assetSymbol,
labelDescription: EST_MARGIN_TOOLTIP_TEXT(assetSymbol),
},
*/
{ {
label: t('Margin required'), label: t('Margin required'),
value: `~${formatValueWithAssetDp( value: `~${formatValueWithAssetDp(
balance currentInitialMargin
? (BigInt(totalMargin) - BigInt(balance)).toString() ? (
: totalMargin BigInt(estimatedTotalInitialMargin) - BigInt(currentInitialMargin)
).toString()
: estimatedTotalInitialMargin
)}`, )}`,
symbol: assetSymbol, symbol: assetSymbol,
labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol), labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol),
}, },
]; ];
if (balance) { if (totalBalance) {
const totalMarginAvailable = (
currentMaintenanceMargin
? totalBalance - BigInt(currentMaintenanceMargin)
: totalBalance
).toString();
details.push({
indent: true,
label: t('Total margin available'),
value: `~${formatValueWithAssetDp(totalMarginAvailable)}`,
symbol: assetSymbol,
labelDescription: TOTAL_MARGIN_AVAILABLE(
formatValueWithAssetDp(generalAccountBalance),
formatValueWithAssetDp(marginAccountBalance),
formatValueWithAssetDp(currentMaintenanceMargin),
assetSymbol
),
});
if (marginAccountBalance) {
const deductionFromCollateral =
BigInt(estimatedTotalInitialMargin) - BigInt(marginAccountBalance);
details.push({
indent: true,
label: t('Deduction from collateral'),
value: `~${formatValueWithAssetDp(
deductionFromCollateral > 0 ? deductionFromCollateral.toString() : '0'
)}`,
symbol: assetSymbol,
labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol),
});
}
details.push({ details.push({
label: t('Projected margin'), label: t('Projected margin'),
value: `~${formatValueWithAssetDp(totalMargin)}`, value: `~${formatValueWithAssetDp(estimatedTotalInitialMargin)}`,
symbol: assetSymbol, symbol: assetSymbol,
labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT, labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT,
}); });
} }
details.push({ details.push({
label: t('Current margin allocation'), label: t('Current margin allocation'),
value: balance value: `${formatValueWithAssetDp(marginAccountBalance)}`,
? `~${formatValueWithAssetDp(balance)}`
: `${formatValueWithAssetDp(balance)}`,
symbol: assetSymbol, symbol: assetSymbol,
labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT, labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT,
}); });

View File

@ -65,5 +65,11 @@ export const useInitialMargin = (
sellMargin > buyMargin ? sellMargin.toString() : buyMargin.toString(); sellMargin > buyMargin ? sellMargin.toString() : buyMargin.toString();
} }
return useMemo(() => ({ totalMargin, margin }), [totalMargin, margin]); return useMemo(
() => ({
totalMargin,
margin,
}),
[totalMargin, margin]
);
}; };

View File

@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { Position } from '../'; import type { Position } from '../';
import { usePositionsData, PositionsTable } from '../'; import { usePositionsData, PositionsTable } from '../';
import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet'; import { useVegaTransactionStore } from '@vegaprotocol/wallet';
@ -23,8 +22,8 @@ export const PositionsManager = ({
noBottomPlaceholder, noBottomPlaceholder,
}: PositionsManagerProps) => { }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const { data, error, loading, reload } = usePositionsData(partyId, gridRef); const { data, error, loading, reload } = usePositionsData(partyId, gridRef);
const [dataCount, setDataCount] = useState(data?.length ?? 0);
const create = useVegaTransactionStore((store) => store.create); const create = useVegaTransactionStore((store) => store.create);
const onClose = ({ const onClose = ({
marketId, marketId,
@ -67,10 +66,7 @@ export const PositionsManager = ({
setId, setId,
disabled: noBottomPlaceholder, disabled: noBottomPlaceholder,
}); });
useEffect(() => { const updateRowCount = useCallback(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []); }, []);
return ( return (
@ -83,7 +79,8 @@ export const PositionsManager = ({
suppressLoadingOverlay suppressLoadingOverlay
suppressNoRowsOverlay suppressNoRowsOverlay
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
onFilterChanged={onFilterChanged} onFilterChanged={updateRowCount}
onRowDataUpdated={updateRowCount}
{...bottomPlaceholderProps} {...bottomPlaceholderProps}
/> />
<div className="pointer-events-none absolute inset-0"> <div className="pointer-events-none absolute inset-0">