feat(trading): margin estimate update (#5664)

Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
Co-authored-by: daro-maj <119658839+daro-maj@users.noreply.github.com>
This commit is contained in:
Bartłomiej Głownia 2024-01-31 15:05:30 +01:00 committed by GitHub
parent 8b91592b93
commit 1780f6fa7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 616 additions and 1083 deletions

View File

@ -38,7 +38,7 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id("toast-content")).to_have_text( expect(page.get_by_test_id("toast-content")).to_have_text(
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x") "ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x")
expect(page.locator(margin_row).nth(1) expect(page.locator(margin_row).nth(1)
).to_have_text("11,109.99996Isolated1.0x") ).to_have_text("22,109.99996Isolated1.0x")
# tbd - tooltip is not visible without this wait # tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000) page.wait_for_timeout(1000)
page.get_by_test_id(tab_positions).get_by_text("Isolated").hover() page.get_by_test_id(tab_positions).get_by_text("Isolated").hover()

View File

@ -23,7 +23,6 @@ def continuous_market(vega):
return setup_continuous_market(vega) return setup_continuous_market(vega)
@pytest.mark.skip("marked id issue #5681")
@pytest.mark.usefixtures("auth", "risk_accepted") @pytest.mark.usefixtures("auth", "risk_accepted")
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page): def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}") page.goto(f"/#/markets/{continuous_market}")

View File

@ -1,15 +0,0 @@
import type { Account } from './accounts-data-provider';
import * as Schema from '@vegaprotocol/types';
interface Props {
accounts: Account[] | null;
marketId: string;
}
export const getMarketAccount = ({ accounts, marketId }: Props) =>
accounts?.find((account) => {
return (
account.market?.id === marketId &&
account.type === Schema.AccountType.ACCOUNT_TYPE_MARGIN
);
}) || null;

View File

@ -6,8 +6,7 @@ export * from './accounts-manager';
export * from './breakdown-table'; export * from './breakdown-table';
export * from './use-account-balance'; export * from './use-account-balance';
export * from './get-settlement-account'; export * from './get-settlement-account';
export * from './use-market-account-balance'; export * from './use-margin-account-balance';
export * from './__generated__/Margins'; export * from './__generated__/Margins';
export { MarginHealthChart } from './margin-health-chart';
export * from './margin-data-provider'; export * from './margin-data-provider';
export * from './transfer-container'; export * from './transfer-container';

View File

@ -83,3 +83,25 @@ export const marketMarginDataProvider = makeDerivedDataProvider<
(margin) => margin.market.id === marketId (margin) => margin.market.id === marketId
) || null ) || null
); );
export type MarginModeData = Pick<
MarginFieldsFragment,
'marginMode' | 'marginFactor'
>;
export const marginModeDataProvider = makeDerivedDataProvider<
MarginModeData,
never,
MarginsQueryVariables & { marketId: string }
>([marketMarginDataProvider], ([data], variables, previousData) =>
produce(previousData, (draft) => {
if (!data) {
return data;
}
const newData = {
marginMode: (data as MarginFieldsFragment).marginMode,
marginFactor: (data as MarginFieldsFragment).marginFactor,
};
return draft ? Object.assign(draft, newData) : newData;
})
);

View File

@ -1,253 +0,0 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketMarginDataProvider } from './margin-data-provider';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useT, ns } from './use-t';
import { useAccountBalance } from './use-account-balance';
import { useMarketAccountBalance } from './use-market-account-balance';
import { Trans } from 'react-i18next';
const MarginHealthChartTooltipRow = ({
label,
value,
decimals,
href,
}: {
label: string;
value: string;
decimals: number;
href?: string;
}) => (
<>
<div
className="float-left clear-left"
key="label"
data-testid="margin-health-tooltip-label"
>
{href ? (
<ExternalLink href={href} target="_blank">
{label}
</ExternalLink>
) : (
label
)}
</div>
<div
className="float-right"
key="value"
data-testid="margin-health-tooltip-value"
>
{addDecimalsFormatNumber(value, decimals)}
</div>
</>
);
export const MarginHealthChartTooltip = ({
maintenanceLevel,
searchLevel,
initialLevel,
collateralReleaseLevel,
decimals,
marginAccountBalance,
}: {
maintenanceLevel: string;
searchLevel: string;
initialLevel: string;
collateralReleaseLevel: string;
decimals: number;
marginAccountBalance?: string;
}) => {
const t = useT();
const tooltipContent = [
<MarginHealthChartTooltipRow
key={'maintenance'}
label={t('maintenance level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance"
value={maintenanceLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'search'}
label={t('search level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-searching-for-collateral"
value={searchLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'initial'}
label={t('initial level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-initial"
value={initialLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'release'}
label={t('release level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-releasing-collateral"
value={collateralReleaseLevel}
decimals={decimals}
/>,
];
if (marginAccountBalance) {
const balance = (
<MarginHealthChartTooltipRow
key={'balance'}
label={t('balance')}
value={marginAccountBalance}
decimals={decimals}
/>
);
if (BigInt(marginAccountBalance) < BigInt(searchLevel)) {
tooltipContent.splice(1, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(initialLevel)) {
tooltipContent.splice(2, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(collateralReleaseLevel)) {
tooltipContent.splice(3, 0, balance);
} else {
tooltipContent.push(balance);
}
}
return (
<div className="overflow-hidden" data-testid="margin-health-tooltip">
{tooltipContent}
</div>
);
};
export const MarginHealthChart = ({
marketId,
assetId,
}: {
marketId: string;
assetId: string;
}) => {
const { data: assetsMap } = useAssetsMapProvider();
const { pubKey: partyId } = useVegaWallet();
const { data } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId, partyId: partyId ?? '' },
skip: !partyId,
});
const { accountBalance: rawGeneralAccountBalance } =
useAccountBalance(assetId);
const { accountBalance: rawMarginAccountBalance } =
useMarketAccountBalance(marketId);
const asset = assetsMap && assetsMap[assetId];
if (!data || !asset) {
return null;
}
const { decimals } = asset;
const collateralReleaseLevel = Number(data.collateralReleaseLevel);
const initialLevel = Number(data.initialLevel);
const maintenanceLevel = Number(data.maintenanceLevel);
const searchLevel = Number(data.searchLevel);
const marginAccountBalance = Number(rawMarginAccountBalance);
const generalAccountBalance = Number(rawGeneralAccountBalance);
const max = Math.max(
marginAccountBalance + generalAccountBalance,
collateralReleaseLevel
);
const red = maintenanceLevel / max;
const orange = (searchLevel - maintenanceLevel) / max;
const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max;
const green = (collateralReleaseLevel - initialLevel) / max + yellow;
const balanceMarker = marginAccountBalance / max;
const tooltip = (
<MarginHealthChartTooltip
maintenanceLevel={data.maintenanceLevel}
searchLevel={data.searchLevel}
initialLevel={data.initialLevel}
collateralReleaseLevel={data.collateralReleaseLevel}
marginAccountBalance={rawMarginAccountBalance}
decimals={decimals}
/>
);
return (
<div data-testid="margin-health-chart">
<Trans
defaults="{{balance}} above <0>maintenance level</0>"
components={[
<ExternalLink href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance">
maintenance level
</ExternalLink>,
]}
values={{
balance: addDecimalsFormatNumber(
(
BigInt(marginAccountBalance) - BigInt(maintenanceLevel)
).toString(),
decimals
),
}}
ns={ns}
/>
<Tooltip description={tooltip}>
<div
data-testid="margin-health-chart-track"
className="relative bg-vega-green-650"
style={{
height: '6px',
marginBottom: '1px',
display: 'flex',
}}
>
<div
data-testid="margin-health-chart-red"
className="bg-vega-red-550"
style={{
height: '100%',
width: `${red * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-orange"
className="bg-vega-orange"
style={{
height: '100%',
width: `${orange * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-yellow"
className="bg-vega-yellow"
style={{
height: '100%',
width: `${yellow * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-green"
className="bg-vega-green-600"
style={{
height: '100%',
width: `${green * 100}%`,
}}
></div>
{balanceMarker > 0 && balanceMarker < 100 && (
<div
data-testid="margin-health-chart-balance"
className="absolute bg-vega-blue"
style={{
height: '8px',
width: '8px',
top: '-1px',
transform: 'translate(-4px, 0px)',
borderRadius: '50%',
border: '1px solid white',
backgroundColor: 'blue',
left: `${balanceMarker * 100}%`,
}}
></div>
)}
</div>
</Tooltip>
</div>
);
};

View File

@ -1,158 +0,0 @@
import {
MarginHealthChart,
MarginHealthChartTooltip,
} from './margin-health-chart';
import { act, render, screen } from '@testing-library/react';
import type { MarginFieldsFragment } from './__generated__/Margins';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { MarginMode } from '@vegaprotocol/types';
const asset: AssetFieldsFragment = {
id: 'assetId',
decimals: 2,
} as AssetFieldsFragment;
const margins: MarginFieldsFragment = {
asset: {
id: 'assetId',
},
collateralReleaseLevel: '1000',
initialLevel: '800',
searchLevel: '600',
maintenanceLevel: '400',
marginFactor: '',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginLevel: '',
market: {
id: 'marketId',
},
};
const mockGetMargins = jest.fn(() => margins);
const mockGetBalance = jest.fn(() => '0');
jest.mock('./margin-data-provider', () => ({}));
jest.mock('@vegaprotocol/assets', () => ({
useAssetsMapProvider: () => {
return {
data: {
assetId: asset,
},
};
},
}));
jest.mock('@vegaprotocol/wallet', () => ({
useVegaWallet: () => {
return {
pubKey: 'partyId',
};
},
}));
jest.mock('@vegaprotocol/data-provider', () => ({
useDataProvider: () => {
return {
data: mockGetMargins(),
};
},
}));
jest.mock('./use-account-balance', () => ({
useAccountBalance: () => {
return {
accountBalance: mockGetBalance(),
};
},
}));
jest.mock('./use-market-account-balance', () => ({
useMarketAccountBalance: () => {
return {
accountBalance: '700',
};
},
}));
describe('MarginHealthChart', () => {
it('should render correct values', async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
const chart = screen.getByTestId('margin-health-chart');
expect(chart).toHaveTextContent('3.00 above maintenance level');
const red = screen.getByTestId('margin-health-chart-red');
const orange = screen.getByTestId('margin-health-chart-orange');
const yellow = screen.getByTestId('margin-health-chart-yellow');
const green = screen.getByTestId('margin-health-chart-green');
const balance = screen.getByTestId('margin-health-chart-balance');
expect(parseInt(red.style.width)).toBe(40);
expect(parseInt(orange.style.width)).toBe(20);
expect(parseInt(yellow.style.width)).toBe(10);
expect(parseInt(green.style.width)).toBe(30);
expect(parseInt(balance.style.left)).toBe(70);
});
it('should use correct scale', async () => {
mockGetBalance.mockReturnValueOnce('1300');
await act(async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
});
await screen.findByTestId('margin-health-chart');
const red = screen.getByTestId('margin-health-chart-red');
expect(parseInt(red.style.width)).toBe(20);
});
});
describe('MarginHealthChartTooltip', () => {
it('renders correct values and labels', async () => {
await act(async () => {
render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="500"
/>
);
});
const labels = await screen.findAllByTestId('margin-health-tooltip-label');
const expectedLabels = [
'maintenance level',
'balance',
'search level',
'initial level',
'release level',
];
labels.forEach((value, i) => {
expect(value).toHaveTextContent(expectedLabels[i]);
});
const values = await screen.findAllByTestId('margin-health-tooltip-value');
const expectedValues = ['4.00', '5.00', '6.00', '8.00', '10.00'];
values.forEach((value, i) => {
expect(value).toHaveTextContent(expectedValues[i]);
});
});
it('renders balance in correct place', async () => {
const { rerender } = render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="700"
/>
);
let values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values[2]).toHaveTextContent('7.00');
rerender(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="900"
/>
);
values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values.length).toBe(5);
expect(values[3]).toHaveTextContent('9.00');
});
});

View File

@ -0,0 +1,68 @@
import { useCallback, useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { accountsDataProvider } from './accounts-data-provider';
import type { Account } from './accounts-data-provider';
import { AccountType } from '@vegaprotocol/types';
export const useMarginAccountBalance = (marketId: string) => {
const { pubKey } = useVegaWallet();
const [marginAccountBalance, setMarginAccountBalance] = useState<string>('');
const [orderMarginAccountBalance, setOrderMarginAccountBalance] =
useState<string>('');
const [accountDecimals, setAccountDecimals] = useState<number | null>(null);
const update = useCallback(
({ data }: { data: Account[] | null }) => {
const marginAccount = data?.find((account) => {
return (
account.market?.id === marketId &&
account.type === AccountType.ACCOUNT_TYPE_MARGIN
);
});
const orderMarginAccount = data?.find((account) => {
return (
account.market?.id === marketId &&
account.type === AccountType.ACCOUNT_TYPE_ORDER_MARGIN
);
});
if (marginAccount?.balance) {
setMarginAccountBalance(marginAccount?.balance || '');
}
if (orderMarginAccount?.balance) {
setOrderMarginAccountBalance(orderMarginAccount?.balance || '');
}
const decimals =
orderMarginAccount?.asset.decimals || marginAccount?.asset.decimals;
if (decimals) {
setAccountDecimals(decimals);
}
return true;
},
[marketId]
);
const { loading, error } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey || !marketId,
update,
});
return useMemo(
() => ({
marginAccountBalance: pubKey ? marginAccountBalance : '',
orderMarginAccountBalance: pubKey ? orderMarginAccountBalance : '',
accountDecimals: pubKey ? accountDecimals : null,
loading,
error,
}),
[
marginAccountBalance,
orderMarginAccountBalance,
accountDecimals,
pubKey,
loading,
error,
]
);
};

View File

@ -1,41 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { accountsDataProvider } from './accounts-data-provider';
import type { Account } from './accounts-data-provider';
import { getMarketAccount } from './get-market-account';
export const useMarketAccountBalance = (marketId: string) => {
const { pubKey } = useVegaWallet();
const [accountBalance, setAccountBalance] = useState<string>('');
const [accountDecimals, setAccountDecimals] = useState<number | null>(null);
const update = useCallback(
({ data }: { data: Account[] | null }) => {
const account = getMarketAccount({ accounts: data, marketId });
if (account?.balance) {
setAccountBalance(account?.balance || '');
}
if (account?.asset.decimals) {
setAccountDecimals(account?.asset.decimals || null);
}
return true;
},
[marketId]
);
const { loading, error } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey || !marketId,
update,
});
return useMemo(
() => ({
accountBalance: pubKey ? accountBalance : '',
accountDecimals: pubKey ? accountDecimals : null,
loading,
error,
}),
[accountBalance, accountDecimals, pubKey, loading, error]
);
};

View File

@ -1,47 +1,14 @@
import { useCallback, useState } from 'react'; import { getAsset } from '@vegaprotocol/markets';
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import type { EstimatePositionQuery } from '@vegaprotocol/positions'; import { formatNumberPercentage, formatValue } from '@vegaprotocol/utils';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import {
formatNumberPercentage,
formatRange,
formatValue,
} from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import * as Schema from '@vegaprotocol/types';
import {
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks/use-estimate-fees'; import { useEstimateFees } from '../../hooks/use-estimate-fees';
import { KeyValue } from './key-value'; import { KeyValue } from './key-value';
import { import { Intent, Pill } from '@vegaprotocol/ui-toolkit';
Accordion,
AccordionChevron,
AccordionPanel,
Intent,
ExternalLink,
Pill,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { FeesBreakdown } from '../fees-breakdown'; import { FeesBreakdown } from '../fees-breakdown';
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts'; import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
import { useT, ns } from '../../use-t'; import { useT } from '../../use-t';
import { Trans } from 'react-i18next';
export const emptyValue = '-'; export const emptyValue = '-';
@ -119,337 +86,3 @@ export const DealTicketFeeDetails = ({
/> />
); );
}; };
export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string;
marginAccountBalance?: string;
market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string;
positionEstimate: EstimatePositionQuery['estimatePosition'];
side: Schema.Side;
}
export const DealTicketMarginDetails = ({
marginAccountBalance,
generalAccountBalance,
assetSymbol,
market,
onMarketClick,
positionEstimate,
side,
}: DealTicketMarginDetailsProps) => {
const t = useT();
const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: partyId || '' },
skip: !partyId,
});
const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalBalance =
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
if (currentMargins) {
marginRequiredBestCase = (
BigInt(marginEstimate.bestCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredBestCase.startsWith('-')) {
marginRequiredBestCase = '0';
}
marginRequiredWorstCase = (
BigInt(marginEstimate.worstCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
}
}
const totalMarginAvailable = (
currentMargins
? totalBalance - BigInt(currentMargins.maintenanceLevel)
: totalBalance
).toString();
let deductionFromCollateral = null;
let projectedMargin = null;
if (marginAccountBalance) {
const deductionFromCollateralBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
const deductionFromCollateralWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
deductionFromCollateral = (
<KeyValue
indent
label={t('Deduction from collateral')}
value={formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals
)}
formattedValue={formatValue(
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
{ assetSymbol }
)}
/>
);
projectedMargin = (
<KeyValue
label={t('Projected margin')}
value={formatRange(
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals
)}
formattedValue={formatValue(
marginEstimate?.worstCase.initialLevel,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
EST_TOTAL_MARGIN_TOOLTIP_TEXT
)}
/>
);
}
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateRange = emptyValue;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCaseIncludingSellOrders = BigInt(
liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCase =
side === Schema.Side.SIDE_BUY
? liquidationEstimateBestCaseIncludingBuyOrders
: liquidationEstimateBestCaseIncludingSellOrders;
const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt(
liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCaseIncludingSellOrders = BigInt(
liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCase =
side === Schema.Side.SIDE_BUY
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
liquidationPriceEstimate = formatValue(
liquidationEstimateWorstCase.toString(),
market.decimalPlaces,
undefined,
market.decimalPlaces
);
liquidationPriceEstimateRange = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
market.decimalPlaces,
undefined,
market.decimalPlaces
);
}
const onAccountBreakdownDialogClose = useCallback(
() => setBreakdownDialog(false),
[]
);
const quoteName = getQuoteName(market);
return (
<div className="flex flex-col w-full gap-2 pt-2">
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center text-left gap-1">
<Tooltip
description={t(
'MARGIN_DIFF_TOOLTIP_TEXT',
MARGIN_DIFF_TOOLTIP_TEXT,
{ assetSymbol }
)}
>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
>
<div className="font-mono text-right">
{formatValue(
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col w-full gap-2">
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'TOTAL_MARGIN_AVAILABLE',
TOTAL_MARGIN_AVAILABLE,
{
generalAccountBalance: formatValue(
generalAccountBalance,
assetDecimals,
quantum
),
marginAccountBalance: formatValue(
marginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol,
}
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue
label={t('Liquidation')}
value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
labelDescription={
<>
<span>
{t(
'LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT',
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT
)}
</span>{' '}
<span>
<Trans
defaults="For full details please see <0>liquidation price estimate documentation</0>."
components={[
<ExternalLink
href={
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
}
>
liquidation price estimate documentation
</ExternalLink>,
]}
ns={ns}
/>
</span>
</>
}
/>
{partyId && (
<AccountBreakdownDialog
assetId={breakdownDialog ? asset.id : undefined}
partyId={partyId}
onMarketClick={onMarketClick}
onClose={onAccountBreakdownDialogClose}
/>
)}
</div>
);
};

View File

@ -26,12 +26,25 @@ import {
import classNames from 'classnames'; import classNames from 'classnames';
import { useT, ns } from '../../use-t'; import { useT, ns } from '../../use-t';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import type { DealTicketMarginDetailsProps } from './deal-ticket-fee-details'; import type { Market } from '@vegaprotocol/markets';
import { emptyValue } from './deal-ticket-fee-details'; import { emptyValue } from './deal-ticket-fee-details';
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string;
marginAccountBalance?: string;
orderMarginAccountBalance?: string;
market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string;
positionEstimate: EstimatePositionQuery['estimatePosition'];
side: Schema.Side;
}
export const DealTicketMarginDetails = ({ export const DealTicketMarginDetails = ({
marginAccountBalance, marginAccountBalance,
generalAccountBalance, generalAccountBalance,
orderMarginAccountBalance,
assetSymbol, assetSymbol,
market, market,
onMarketClick, onMarketClick,
@ -48,31 +61,44 @@ export const DealTicketMarginDetails = ({
}); });
const liquidationEstimate = positionEstimate?.liquidation; const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin; const marginEstimate = positionEstimate?.margin;
const totalMarginAccountBalance =
BigInt(marginAccountBalance || '0') +
BigInt(orderMarginAccountBalance || '0');
const totalBalance = const totalBalance =
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0'); BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
const asset = getAsset(market); const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset; const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined; let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined; let marginRequiredWorstCase: string | undefined = undefined;
const marginEstimateBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) +
BigInt(marginEstimate?.bestCase.orderMarginLevel ?? 0);
const marginEstimateWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) +
BigInt(marginEstimate?.worstCase.orderMarginLevel ?? 0);
if (marginEstimate) { if (marginEstimate) {
if (currentMargins) { if (currentMargins) {
const currentMargin =
BigInt(currentMargins.initialLevel) +
BigInt(currentMargins.orderMarginLevel);
marginRequiredBestCase = ( marginRequiredBestCase = (
BigInt(marginEstimate.bestCase.initialLevel) - marginEstimateBestCase - currentMargin
BigInt(currentMargins.initialLevel)
).toString(); ).toString();
if (marginRequiredBestCase.startsWith('-')) { if (marginRequiredBestCase.startsWith('-')) {
marginRequiredBestCase = '0'; marginRequiredBestCase = '0';
} }
marginRequiredWorstCase = ( marginRequiredWorstCase = (
BigInt(marginEstimate.worstCase.initialLevel) - marginEstimateWorstCase - currentMargin
BigInt(currentMargins.initialLevel)
).toString(); ).toString();
if (marginRequiredWorstCase.startsWith('-')) { if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0'; marginRequiredWorstCase = '0';
} }
} else { } else {
marginRequiredBestCase = marginEstimate.bestCase.initialLevel; marginRequiredBestCase = marginEstimateBestCase.toString();
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel; marginRequiredWorstCase = marginEstimateWorstCase.toString();
} }
} }
@ -84,14 +110,12 @@ export const DealTicketMarginDetails = ({
let deductionFromCollateral = null; let deductionFromCollateral = null;
let projectedMargin = null; let projectedMargin = null;
if (marginAccountBalance) { if (totalMarginAccountBalance) {
const deductionFromCollateralBestCase = const deductionFromCollateralBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) - marginEstimateBestCase - totalMarginAccountBalance;
BigInt(marginAccountBalance);
const deductionFromCollateralWorstCase = const deductionFromCollateralWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) - marginEstimateWorstCase - totalMarginAccountBalance;
BigInt(marginAccountBalance);
deductionFromCollateral = ( deductionFromCollateral = (
<KeyValue <KeyValue
@ -125,12 +149,12 @@ export const DealTicketMarginDetails = ({
<KeyValue <KeyValue
label={t('Projected margin')} label={t('Projected margin')}
value={formatRange( value={formatRange(
marginEstimate?.bestCase.initialLevel, marginEstimateBestCase.toString(),
marginEstimate?.worstCase.initialLevel, marginEstimateWorstCase.toString(),
assetDecimals assetDecimals
)} )}
formattedValue={formatValue( formattedValue={formatValue(
marginEstimate?.worstCase.initialLevel, marginEstimateWorstCase.toString(),
assetDecimals, assetDecimals,
quantum quantum
)} )}
@ -276,6 +300,11 @@ export const DealTicketMarginDetails = ({
assetDecimals, assetDecimals,
quantum quantum
), ),
orderMarginAccountBalance: formatValue(
orderMarginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue( marginMaintenance: formatValue(
currentMargins?.maintenanceLevel, currentMargins?.maintenanceLevel,
assetDecimals, assetDecimals,
@ -294,14 +323,17 @@ export const DealTicketMarginDetails = ({
? () => setBreakdownDialog(true) ? () => setBreakdownDialog(true)
: undefined : undefined
} }
value={formatValue(marginAccountBalance, assetDecimals)} value={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals
)}
symbol={assetSymbol} symbol={assetSymbol}
labelDescription={t( labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT', 'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT MARGIN_ACCOUNT_TOOLTIP_TEXT
)} )}
formattedValue={formatValue( formattedValue={formatValue(
marginAccountBalance, totalMarginAccountBalance.toString(),
assetDecimals, assetDecimals,
quantum quantum
)} )}

View File

@ -58,8 +58,9 @@ import type {
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarginWarning } from '../deal-ticket-validation/margin-warning'; import { MarginWarning } from '../deal-ticket-validation/margin-warning';
import { import {
useMarketAccountBalance, useMarginAccountBalance,
useAccountBalance, useAccountBalance,
marginModeDataProvider,
} from '@vegaprotocol/accounts'; } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { type OrderFormValues } from '../../hooks'; import { type OrderFormValues } from '../../hooks';
@ -166,9 +167,10 @@ export const DealTicket = ({
const asset = getAsset(market); const asset = getAsset(market);
const { const {
accountBalance: marginAccountBalance, orderMarginAccountBalance,
marginAccountBalance,
loading: loadingMarginAccountBalance, loading: loadingMarginAccountBalance,
} = useMarketAccountBalance(market.id); } = useMarginAccountBalance(market.id);
const { const {
accountBalance: generalAccountBalance, accountBalance: generalAccountBalance,
@ -176,7 +178,9 @@ export const DealTicket = ({
} = useAccountBalance(asset.id); } = useAccountBalance(asset.id);
const balance = ( const balance = (
BigInt(marginAccountBalance) + BigInt(generalAccountBalance) BigInt(marginAccountBalance) +
BigInt(generalAccountBalance) +
BigInt(orderMarginAccountBalance)
).toString(); ).toString();
const { marketState, marketTradingMode } = marketData; const { marketState, marketTradingMode } = marketData;
@ -241,7 +245,19 @@ export const DealTicket = ({
variables: { partyId: pubKey || '', marketId: market.id }, variables: { partyId: pubKey || '', marketId: market.id },
skip: !pubKey, skip: !pubKey,
}); });
const openVolume = useOpenVolume(pubKey, market.id) ?? '0'; const { data: margin } = useDataProvider({
dataProvider: marginModeDataProvider,
variables: { partyId: pubKey || '', marketId: market.id },
skip: !pubKey,
});
const { openVolume, averageEntryPrice } = useOpenVolume(
pubKey,
market.id
) || {
openVolume: '0',
averageEntryPrice: '0',
};
const orders = activeOrders const orders = activeOrders
? activeOrders.map<Schema.OrderInfo>((order) => ({ ? activeOrders.map<Schema.OrderInfo>((order) => ({
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET, isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
@ -259,21 +275,25 @@ export const DealTicket = ({
}); });
} }
const positionEstimate = usePositionEstimate({ const positionEstimate = usePositionEstimate(
marketId: market.id, {
openVolume, marketId: market.id,
orders, openVolume,
marginAccountBalance: marginAccountBalance, averageEntryPrice,
generalAccountBalance: generalAccountBalance, orders,
orderMarginAccountBalance: '0', // TODO: Get real balance marginAccountBalance: marginAccountBalance || '0',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, // TODO: unhardcode this and get users margin mode for the market generalAccountBalance: generalAccountBalance || '0',
averageEntryPrice: marketPrice || '0', // TODO: This assumes the order will be entirely filled at the current market price orderMarginAccountBalance: orderMarginAccountBalance || '0',
skip: marginFactor: margin?.marginFactor || '1',
!normalizedOrder || marginMode:
margin?.marginMode || Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
includeCollateralIncreaseInAvailableCollateral: true,
},
!normalizedOrder ||
(normalizedOrder.type !== Schema.OrderType.TYPE_MARKET && (normalizedOrder.type !== Schema.OrderType.TYPE_MARKET &&
(!normalizedOrder.price || normalizedOrder.price === '0')) || (!normalizedOrder.price || normalizedOrder.price === '0')) ||
normalizedOrder.size === '0', normalizedOrder.size === '0'
}); );
const assetSymbol = getAsset(market).symbol; const assetSymbol = getAsset(market).symbol;
@ -319,7 +339,9 @@ export const DealTicket = ({
} }
const hasNoBalance = const hasNoBalance =
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance); !BigInt(generalAccountBalance) &&
!BigInt(marginAccountBalance) &&
!BigInt(orderMarginAccountBalance);
if ( if (
hasNoBalance && hasNoBalance &&
!(loadingMarginAccountBalance || loadingGeneralAccountBalance) !(loadingMarginAccountBalance || loadingGeneralAccountBalance)
@ -349,6 +371,7 @@ export const DealTicket = ({
marketTradingMode, marketTradingMode,
generalAccountBalance, generalAccountBalance,
marginAccountBalance, marginAccountBalance,
orderMarginAccountBalance,
loadingMarginAccountBalance, loadingMarginAccountBalance,
loadingGeneralAccountBalance, loadingGeneralAccountBalance,
pubKey, pubKey,
@ -707,10 +730,16 @@ export const DealTicket = ({
asset={asset} asset={asset}
marketTradingMode={marketData.marketTradingMode} marketTradingMode={marketData.marketTradingMode}
balance={balance} balance={balance}
margin={ margin={(
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel || BigInt(
'0' positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
} '0'
) +
BigInt(
positionEstimate?.estimatePosition?.margin.bestCase
.orderMarginLevel || '0'
)
).toString()}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
pubKey={pubKey} pubKey={pubKey}
onDeposit={onDeposit} onDeposit={onDeposit}
@ -743,6 +772,7 @@ export const DealTicket = ({
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
assetSymbol={asset.symbol} assetSymbol={asset.symbol}
marginAccountBalance={marginAccountBalance} marginAccountBalance={marginAccountBalance}
orderMarginAccountBalance={orderMarginAccountBalance}
generalAccountBalance={generalAccountBalance} generalAccountBalance={generalAccountBalance}
positionEstimate={positionEstimate?.estimatePosition} positionEstimate={positionEstimate?.estimatePosition}
market={market} market={market}
@ -768,8 +798,20 @@ interface SummaryMessageProps {
export const NoWalletWarning = ({ export const NoWalletWarning = ({
isReadOnly, isReadOnly,
}: Pick<SummaryMessageProps, 'isReadOnly'>) => { noWalletConnected,
}: Pick<SummaryMessageProps, 'isReadOnly'> & {
noWalletConnected?: boolean;
}) => {
const t = useT(); const t = useT();
if (noWalletConnected) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
{t('You need a Vega wallet to start trading on this market')}
</InputError>
</div>
);
}
if (isReadOnly) { if (isReadOnly) {
return ( return (
<div className="mb-2"> <div className="mb-2">

View File

@ -1,9 +1,12 @@
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import * as Schema from '@vegaprotocol/types';
import { import {
TradingButton as Button, TradingButton as Button,
TradingInput as Input, TradingInput as Input,
FormGroup, FormGroup,
LeverageSlider, LeverageSlider,
Notification,
Intent,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet'; import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet';
import * as Types from '@vegaprotocol/types'; import * as Types from '@vegaprotocol/types';
@ -15,15 +18,151 @@ import { Dialog } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useT } from '../../use-t'; import { useT } from '../../use-t';
import classnames from 'classnames'; import classnames from 'classnames';
import { marketMarginDataProvider } from '@vegaprotocol/accounts'; import {
import { useMaxLeverage } from '@vegaprotocol/positions'; marginModeDataProvider,
useAccountBalance,
useMarginAccountBalance,
} from '@vegaprotocol/accounts';
import { useMaxLeverage, useOpenVolume } from '@vegaprotocol/positions';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { usePositionEstimate } from '../../hooks/use-position-estimate';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { getAsset, useMarket } from '@vegaprotocol/markets';
import { NoWalletWarning } from './deal-ticket';
const defaultLeverage = 10; const defaultLeverage = 10;
export const MarginChange = ({
partyId,
marketId,
marginMode,
marginFactor,
}: {
partyId: string | null;
marketId: string;
marginMode: Types.MarginMode;
marginFactor: string;
}) => {
const t = useT();
const { data: market } = useMarket(marketId);
const asset = market && getAsset(market);
const {
marginAccountBalance,
orderMarginAccountBalance,
loading: marginAccountBalanceLoading,
} = useMarginAccountBalance(marketId);
const {
accountBalance: generalAccountBalance,
loading: generalAccountBalanceLoading,
} = useAccountBalance(asset?.id);
const { openVolume, averageEntryPrice } = useOpenVolume(
partyId,
marketId
) || {
openVolume: '0',
averageEntryPrice: '0',
};
const { data: activeOrders } = useDataProvider({
dataProvider: activeOrdersProvider,
variables: { partyId: partyId || '', marketId },
});
const orders = activeOrders
? activeOrders.map<Schema.OrderInfo>((order) => ({
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
price: order.price,
remaining: order.remaining,
side: order.side,
}))
: [];
const skip =
(!orders?.length && openVolume === '0') ||
marginAccountBalanceLoading ||
generalAccountBalanceLoading;
const estimateMargin = usePositionEstimate(
{
generalAccountBalance: generalAccountBalance || '0',
marginAccountBalance: marginAccountBalance || '0',
marginFactor,
marginMode,
averageEntryPrice,
openVolume,
marketId,
orderMarginAccountBalance: orderMarginAccountBalance || '0',
includeCollateralIncreaseInAvailableCollateral: true,
orders,
},
skip
);
if (
!asset ||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
) {
return null;
}
const collateralIncreaseEstimate = BigInt(
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
);
if (!collateralIncreaseEstimate) {
return null;
}
let positionWarning = '';
if (orders?.length && openVolume !== '0') {
positionWarning = t(
'youHaveOpenPositionAndOrders',
'You have an existing position and open orders on this market.',
{
count: orders.length,
}
);
} else if (!orders?.length) {
positionWarning = t('You have an existing position on this market.');
} else {
positionWarning = t(
'youHaveOpenOrders',
'You have open orders on this market.',
{
count: orders.length,
}
);
}
let marginChangeWarning = '';
const amount = addDecimalsFormatNumber(
collateralIncreaseEstimate.toString(),
asset?.decimals
);
const { symbol } = asset;
const interpolation = { amount, symbol };
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
marginChangeWarning = t(
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
interpolation
);
} else {
marginChangeWarning = t(
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
interpolation
);
}
return (
<div className="mb-2">
<Notification
intent={Intent.Warning}
message={
<>
<p>{positionWarning}</p>
<p>{marginChangeWarning}</p>
</>
}
/>
</div>
);
};
interface MarginDialogProps { interface MarginDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
marketId: string; marketId: string;
partyId: string;
create: VegaTransactionStore['create']; create: VegaTransactionStore['create'];
} }
@ -33,6 +172,7 @@ const CrossMarginModeDialog = ({
marketId, marketId,
create, create,
}: MarginDialogProps) => { }: MarginDialogProps) => {
const { pubKey: partyId, isReadOnly } = useVegaWallet();
const t = useT(); const t = useT();
return ( return (
<Dialog <Dialog
@ -60,15 +200,24 @@ const CrossMarginModeDialog = ({
)} )}
</p> </p>
</div> </div>
<MarginChange
marketId={marketId}
partyId={partyId}
marginMode={Types.MarginMode.MARGIN_MODE_CROSS_MARGIN}
marginFactor="1"
/>
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
<Button <Button
className="w-full" className="w-full"
onClick={() => { onClick={() => {
create({ partyId &&
updateMarginMode: { !isReadOnly &&
marketId, create({
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN, updateMarginMode: {
}, marketId,
}); mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
});
onClose(); onClose();
}} }}
> >
@ -82,10 +231,10 @@ const IsolatedMarginModeDialog = ({
open, open,
onClose, onClose,
marketId, marketId,
partyId,
marginFactor, marginFactor,
create, create,
}: MarginDialogProps & { marginFactor: string }) => { }: MarginDialogProps & { marginFactor: string }) => {
const { pubKey: partyId, isReadOnly } = useVegaWallet();
const [leverage, setLeverage] = useState( const [leverage, setLeverage] = useState(
Number((1 / Number(marginFactor)).toFixed(1)) Number((1 / Number(marginFactor)).toFixed(1))
); );
@ -129,13 +278,15 @@ const IsolatedMarginModeDialog = ({
</div> </div>
<form <form
onSubmit={() => { onSubmit={() => {
create({ partyId &&
updateMarginMode: { !isReadOnly &&
marketId, create({
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN, updateMarginMode: {
marginFactor: `${1 / leverage}`, marketId,
}, mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
}); marginFactor: `${1 / leverage}`,
},
});
onClose(); onClose();
}} }}
> >
@ -144,7 +295,7 @@ const IsolatedMarginModeDialog = ({
<LeverageSlider <LeverageSlider
max={max} max={max}
step={0.1} step={0.1}
value={[leverage]} value={[leverage || 1]}
onValueChange={([value]) => setLeverage(value)} onValueChange={([value]) => setLeverage(value)}
/> />
</div> </div>
@ -154,10 +305,17 @@ const IsolatedMarginModeDialog = ({
min={1} min={1}
max={max} max={max}
step={0.1} step={0.1}
value={leverage} value={leverage || ''}
onChange={(e) => setLeverage(Number(e.target.value))} onChange={(e) => setLeverage(Number(e.target.value))}
/> />
</FormGroup> </FormGroup>
<MarginChange
marketId={marketId}
partyId={partyId}
marginMode={Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN}
marginFactor={`${1 / leverage}`}
/>
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
<Button className="w-full" type="submit"> <Button className="w-full" type="submit">
{t('Confirm')} {t('Confirm')}
</Button> </Button>
@ -169,27 +327,21 @@ const IsolatedMarginModeDialog = ({
export const MarginModeSelector = ({ marketId }: { marketId: string }) => { export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
const t = useT(); const t = useT();
const [dialog, setDialog] = useState<'cross' | 'isolated' | ''>(); const [dialog, setDialog] = useState<'cross' | 'isolated' | ''>();
const { pubKey: partyId, isReadOnly } = useVegaWallet(); const { pubKey: partyId } = useVegaWallet();
const { data: margin } = useDataProvider({ const { data: margin } = useDataProvider({
dataProvider: marketMarginDataProvider, dataProvider: marginModeDataProvider,
variables: { variables: {
partyId: partyId || '', partyId: partyId || '',
marketId, marketId,
}, },
skip: !partyId, skip: !partyId,
}); });
useEffect(() => {
if (!partyId) {
setDialog('');
}
}, [partyId]);
const create = useVegaTransactionStore((state) => state.create); const create = useVegaTransactionStore((state) => state.create);
const marginMode = margin?.marginMode; const marginMode = margin?.marginMode;
const marginFactor = const marginFactor =
margin?.marginFactor && margin?.marginFactor !== '0' margin?.marginFactor && margin?.marginFactor !== '0'
? margin?.marginFactor ? margin?.marginFactor
: undefined; : undefined;
const disabled = isReadOnly;
const onClose = () => setDialog(undefined); const onClose = () => setDialog(undefined);
const enabledModeClassName = 'bg-vega-clight-500 dark:bg-vega-cdark-500'; const enabledModeClassName = 'bg-vega-clight-500 dark:bg-vega-cdark-500';
@ -197,8 +349,8 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
<> <>
<div className="mb-4 grid h-8 leading-8 font-alpha text-xs grid-cols-2"> <div className="mb-4 grid h-8 leading-8 font-alpha text-xs grid-cols-2">
<button <button
disabled={disabled} type="button"
onClick={() => partyId && setDialog('cross')} onClick={() => setDialog('cross')}
className={classnames('rounded', { className={classnames('rounded', {
[enabledModeClassName]: [enabledModeClassName]:
!marginMode || !marginMode ||
@ -208,8 +360,8 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
{t('Cross')} {t('Cross')}
</button> </button>
<button <button
disabled={disabled} type="button"
onClick={() => partyId && setDialog('isolated')} onClick={() => setDialog('isolated')}
className={classnames('rounded', { className={classnames('rounded', {
[enabledModeClassName]: [enabledModeClassName]:
marginMode === Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN, marginMode === Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
@ -222,25 +374,23 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
})} })}
</button> </button>
</div> </div>
{partyId && ( {
<CrossMarginModeDialog <CrossMarginModeDialog
partyId={partyId}
open={dialog === 'cross'} open={dialog === 'cross'}
onClose={onClose} onClose={onClose}
marketId={marketId} marketId={marketId}
create={create} create={create}
/> />
)} }
{partyId && ( {
<IsolatedMarginModeDialog <IsolatedMarginModeDialog
partyId={partyId}
open={dialog === 'isolated'} open={dialog === 'isolated'}
onClose={onClose} onClose={onClose}
marketId={marketId} marketId={marketId}
create={create} create={create}
marginFactor={marginFactor || `${1 / defaultLeverage}`} marginFactor={marginFactor || `${1 / defaultLeverage}`}
/> />
)} }
</> </>
); );
}; };

View File

@ -8,7 +8,7 @@ export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT =
'To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.'; 'To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.';
export const TOTAL_MARGIN_AVAILABLE = export const TOTAL_MARGIN_AVAILABLE =
'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).'; 'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).';
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = export const CONTRACTS_MARGIN_TOOLTIP_TEXT =
'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.';

View File

@ -5,37 +5,15 @@ import {
import { useEstimatePositionQuery } from '@vegaprotocol/positions'; import { useEstimatePositionQuery } from '@vegaprotocol/positions';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface PositionEstimateProps extends EstimatePositionQueryVariables { export const usePositionEstimate = (
skip: boolean; variables: EstimatePositionQueryVariables,
} skip: boolean
) => {
export const usePositionEstimate = ({
marketId,
openVolume,
orders,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
averageEntryPrice,
marginMode,
marginFactor,
skip,
}: PositionEstimateProps) => {
const [estimates, setEstimates] = useState<EstimatePositionQuery | undefined>( const [estimates, setEstimates] = useState<EstimatePositionQuery | undefined>(
undefined undefined
); );
const { data } = useEstimatePositionQuery({ const { data } = useEstimatePositionQuery({
variables: { variables,
marketId,
openVolume,
orders,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
averageEntryPrice,
marginMode,
marginFactor,
},
skip, skip,
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
}); });

View File

@ -13,6 +13,8 @@
"Any orders placed now will not trade until the auction ends": "Any orders placed now will not trade until the auction ends", "Any orders placed now will not trade until the auction ends": "Any orders placed now will not trade until the auction ends",
"below": "below", "below": "below",
"Cancel": "Cancel", "Cancel": "Cancel",
"Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.",
"Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.",
"Closed": "Closed", "Closed": "Closed",
"Closing on {{time}}": "Closing on {{time}}", "Closing on {{time}}": "Closing on {{time}}",
"Confirm": "Confirm", "Confirm": "Confirm",
@ -67,6 +69,13 @@
"One cancels another": "One cancels another", "One cancels another": "One cancels another",
"Only limit orders are permitted when market is in auction": "Only limit orders are permitted when market is in auction", "Only limit orders are permitted when market is in auction": "Only limit orders are permitted when market is in auction",
"Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.": "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.", "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.": "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.",
"You have an existing position on this market.": "You have an existing position on this market.",
"youHaveOpenOrders_one": "You have an open order on this market.",
"youHaveOpenOrders_other": "You have open orders on this market.",
"youHaveOpenOrders": "You have open orders on this market.",
"youHaveOpenPositionAndOrders_one": "You have an existing position and and open order on this market.",
"youHaveOpenPositionAndOrders_other": "You have an existing position and open orders on this market.",
"youHaveOpenPositionAndOrders": "You have an existing position and open orders on this market.",
"Peak size": "Peak size", "Peak size": "Peak size",
"Peak size cannot be greater than the size ({{size}})": "Peak size cannot be greater than the size ({{size}})", "Peak size cannot be greater than the size ({{size}})": "Peak size cannot be greater than the size ({{size}})",
"Peak size cannot be lower than {{stepSize}}": "Peak size cannot be lower than {{stepSize}}", "Peak size cannot be lower than {{stepSize}}": "Peak size cannot be lower than {{stepSize}}",
@ -124,7 +133,7 @@
"Total": "Total", "Total": "Total",
"Total fees": "Total fees", "Total fees": "Total fees",
"Total margin available": "Total margin available", "Total margin available": "Total margin available",
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).", "TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
"No trading": "No trading", "No trading": "No trading",
"Trailing percent offset cannot be higher than 99.9": "Trailing percent offset cannot be higher than 99.9", "Trailing percent offset cannot be higher than 99.9": "Trailing percent offset cannot be higher than 99.9",
"Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}": "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}", "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}": "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}",
@ -140,8 +149,10 @@
"You are setting this market to cross-margin mode.": "You are setting this market to cross-margin mode.", "You are setting this market to cross-margin mode.": "You are setting this market to cross-margin mode.",
"You are setting this market to isolated margin mode.": "You are setting this market to isolated margin mode.", "You are setting this market to isolated margin mode.": "You are setting this market to isolated margin mode.",
"You have only {{amount}}.": "You have only {{amount}}.", "You have only {{amount}}.": "You have only {{amount}}.",
"You have an existing position and open orders on this market": "You have an existing position and open orders on this market",
"You may not have enough margin available to open this position.": "You may not have enough margin available to open this position.", "You may not have enough margin available to open this position.": "You may not have enough margin available to open this position.",
"You need {{symbol}} in your wallet to trade in this market.": "You need {{symbol}} in your wallet to trade in this market.", "You need {{symbol}} in your wallet to trade in this market.": "You need {{symbol}} in your wallet to trade in this market.",
"You need a Vega wallet to start trading on this market": "You need a Vega wallet to start trading on this market",
"You need provide a expiry time/date": "You need provide a expiry time/date", "You need provide a expiry time/date": "You need provide a expiry time/date",
"You need provide a price": "You need provide a price", "You need provide a price": "You need provide a price",
"You need provide a trailing percent offset": "You need provide a trailing percent offset", "You need provide a trailing percent offset": "You need provide a trailing percent offset",

View File

@ -41,24 +41,26 @@ subscription PositionsSubscription($partyId: ID!) {
query EstimatePosition( query EstimatePosition(
$marketId: ID! $marketId: ID!
$openVolume: String! $openVolume: String!
$orders: [OrderInfo!]
$averageEntryPrice: String! $averageEntryPrice: String!
$orders: [OrderInfo!]
$marginAccountBalance: String! $marginAccountBalance: String!
$generalAccountBalance: String! $generalAccountBalance: String!
$orderMarginAccountBalance: String! $orderMarginAccountBalance: String!
$marginMode: MarginMode! $marginMode: MarginMode!
$marginFactor: String $marginFactor: String
$includeCollateralIncreaseInAvailableCollateral: Boolean
) { ) {
estimatePosition( estimatePosition(
marketId: $marketId marketId: $marketId
openVolume: $openVolume openVolume: $openVolume
orders: $orders
averageEntryPrice: $averageEntryPrice averageEntryPrice: $averageEntryPrice
orders: $orders
marginAccountBalance: $marginAccountBalance marginAccountBalance: $marginAccountBalance
generalAccountBalance: $generalAccountBalance generalAccountBalance: $generalAccountBalance
orderMarginAccountBalance: $orderMarginAccountBalance orderMarginAccountBalance: $orderMarginAccountBalance
marginMode: $marginMode marginMode: $marginMode
marginFactor: $marginFactor marginFactor: $marginFactor
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
# Everywhere in the codebase we expect price values of the underlying to have the right # Everywhere in the codebase we expect price values of the underlying to have the right
# number of digits for formatting with market.decimalPlaces. By default the estimatePosition # number of digits for formatting with market.decimalPlaces. By default the estimatePosition
# query will return a full value requiring formatting using asset.decimals. For consistency # query will return a full value requiring formatting using asset.decimals. For consistency
@ -71,14 +73,24 @@ query EstimatePosition(
searchLevel searchLevel
initialLevel initialLevel
collateralReleaseLevel collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
} }
bestCase { bestCase {
maintenanceLevel maintenanceLevel
searchLevel searchLevel
initialLevel initialLevel
collateralReleaseLevel collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
} }
} }
collateralIncreaseEstimate {
worstCase
bestCase
}
liquidation { liquidation {
worstCase { worstCase {
open_volume_only open_volume_only

View File

@ -22,17 +22,18 @@ export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', p
export type EstimatePositionQueryVariables = Types.Exact<{ export type EstimatePositionQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
openVolume: Types.Scalars['String']; openVolume: Types.Scalars['String'];
orders?: Types.InputMaybe<Array<Types.OrderInfo> | Types.OrderInfo>;
averageEntryPrice: Types.Scalars['String']; averageEntryPrice: Types.Scalars['String'];
orders?: Types.InputMaybe<Array<Types.OrderInfo> | Types.OrderInfo>;
marginAccountBalance: Types.Scalars['String']; marginAccountBalance: Types.Scalars['String'];
generalAccountBalance: Types.Scalars['String']; generalAccountBalance: Types.Scalars['String'];
orderMarginAccountBalance: Types.Scalars['String']; orderMarginAccountBalance: Types.Scalars['String'];
marginMode: Types.MarginMode; marginMode: Types.MarginMode;
marginFactor?: Types.InputMaybe<Types.Scalars['String']>; marginFactor?: Types.InputMaybe<Types.Scalars['String']>;
includeCollateralIncreaseInAvailableCollateral?: Types.InputMaybe<Types.Scalars['Boolean']>;
}>; }>;
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string } }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null }; export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string } }, collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
export const PositionFieldsFragmentDoc = gql` export const PositionFieldsFragmentDoc = gql`
fragment PositionFields on Position { fragment PositionFields on Position {
@ -129,17 +130,18 @@ export function usePositionsSubscriptionSubscription(baseOptions: Apollo.Subscri
export type PositionsSubscriptionSubscriptionHookResult = ReturnType<typeof usePositionsSubscriptionSubscription>; export type PositionsSubscriptionSubscriptionHookResult = ReturnType<typeof usePositionsSubscriptionSubscription>;
export type PositionsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<PositionsSubscriptionSubscription>; export type PositionsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<PositionsSubscriptionSubscription>;
export const EstimatePositionDocument = gql` export const EstimatePositionDocument = gql`
query EstimatePosition($marketId: ID!, $openVolume: String!, $orders: [OrderInfo!], $averageEntryPrice: String!, $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String) { query EstimatePosition($marketId: ID!, $openVolume: String!, $averageEntryPrice: String!, $orders: [OrderInfo!], $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String, $includeCollateralIncreaseInAvailableCollateral: Boolean) {
estimatePosition( estimatePosition(
marketId: $marketId marketId: $marketId
openVolume: $openVolume openVolume: $openVolume
orders: $orders
averageEntryPrice: $averageEntryPrice averageEntryPrice: $averageEntryPrice
orders: $orders
marginAccountBalance: $marginAccountBalance marginAccountBalance: $marginAccountBalance
generalAccountBalance: $generalAccountBalance generalAccountBalance: $generalAccountBalance
orderMarginAccountBalance: $orderMarginAccountBalance orderMarginAccountBalance: $orderMarginAccountBalance
marginMode: $marginMode marginMode: $marginMode
marginFactor: $marginFactor marginFactor: $marginFactor
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
scaleLiquidationPriceToMarketDecimals: true scaleLiquidationPriceToMarketDecimals: true
) { ) {
margin { margin {
@ -148,14 +150,24 @@ export const EstimatePositionDocument = gql`
searchLevel searchLevel
initialLevel initialLevel
collateralReleaseLevel collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
} }
bestCase { bestCase {
maintenanceLevel maintenanceLevel
searchLevel searchLevel
initialLevel initialLevel
collateralReleaseLevel collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
} }
} }
collateralIncreaseEstimate {
worstCase
bestCase
}
liquidation { liquidation {
worstCase { worstCase {
open_volume_only open_volume_only
@ -186,13 +198,14 @@ export const EstimatePositionDocument = gql`
* variables: { * variables: {
* marketId: // value for 'marketId' * marketId: // value for 'marketId'
* openVolume: // value for 'openVolume' * openVolume: // value for 'openVolume'
* orders: // value for 'orders'
* averageEntryPrice: // value for 'averageEntryPrice' * averageEntryPrice: // value for 'averageEntryPrice'
* orders: // value for 'orders'
* marginAccountBalance: // value for 'marginAccountBalance' * marginAccountBalance: // value for 'marginAccountBalance'
* generalAccountBalance: // value for 'generalAccountBalance' * generalAccountBalance: // value for 'generalAccountBalance'
* orderMarginAccountBalance: // value for 'orderMarginAccountBalance' * orderMarginAccountBalance: // value for 'orderMarginAccountBalance'
* marginMode: // value for 'marginMode' * marginMode: // value for 'marginMode'
* marginFactor: // value for 'marginFactor' * marginFactor: // value for 'marginFactor'
* includeCollateralIncreaseInAvailableCollateral: // value for 'includeCollateralIncreaseInAvailableCollateral'
* }, * },
* }); * });
*/ */

View File

@ -1,6 +1,7 @@
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import type { EstimatePositionQuery } from './__generated__/Positions'; import type { EstimatePositionQuery } from './__generated__/Positions';
import { MarginMode } from '@vegaprotocol/types';
export const estimatePositionQuery = ( export const estimatePositionQuery = (
override?: PartialDeep<EstimatePositionQuery> override?: PartialDeep<EstimatePositionQuery>
@ -14,14 +15,24 @@ export const estimatePositionQuery = (
initialLevel: '500000', initialLevel: '500000',
maintenanceLevel: '200000', maintenanceLevel: '200000',
searchLevel: '300000', searchLevel: '300000',
marginFactor: '1',
orderMarginLevel: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
}, },
worstCase: { worstCase: {
collateralReleaseLevel: '1100000', collateralReleaseLevel: '1100000',
initialLevel: '600000', initialLevel: '600000',
maintenanceLevel: '300000', maintenanceLevel: '300000',
searchLevel: '400000', searchLevel: '400000',
marginFactor: '1',
orderMarginLevel: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
}, },
}, },
collateralIncreaseEstimate: {
bestCase: '0',
worstCase: '0',
},
liquidation: { liquidation: {
bestCase: { bestCase: {
including_buy_orders: '1', including_buy_orders: '1',

View File

@ -9,32 +9,23 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { MarginMode } from '@vegaprotocol/types'; import { MarginMode } from '@vegaprotocol/types';
describe('LiquidationPrice', () => { describe('LiquidationPrice', () => {
const props = { const variables = {
marketId: 'market-id', marketId: 'market-id',
openVolume: '100', openVolume: '100',
decimalPlaces: 2, averageEntryPrice: '10',
averageEntryPrice: '100', marginAccountBalance: '500',
generalAccountBalance: '100', generalAccountBalance: '500',
marginAccountBalance: '100', orderMarginAccountBalance: '0',
orderMarginAccountBalance: '100',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN, marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
marginFactor: '1', marginFactor: '1',
}; };
const props = { ...variables, decimalPlaces: 2 };
const worstCaseOpenVolume = '200'; const worstCaseOpenVolume = '200';
const bestCaseOpenVolume = '100'; const bestCaseOpenVolume = '100';
const mock: MockedResponse<EstimatePositionQuery> = { const mock: MockedResponse<EstimatePositionQuery> = {
request: { request: {
query: EstimatePositionDocument, query: EstimatePositionDocument,
variables: { variables,
marketId: props.marketId,
openVolume: props.openVolume,
averageEntryPrice: props.averageEntryPrice,
generalAccountBalance: props.generalAccountBalance,
marginAccountBalance: props.marginAccountBalance,
orderMarginAccountBalance: props.orderMarginAccountBalance,
marginMode: props.marginMode,
marginFactor: props.marginFactor,
},
}, },
result: { result: {
data: { data: {
@ -45,14 +36,24 @@ describe('LiquidationPrice', () => {
searchLevel: '100', searchLevel: '100',
initialLevel: '100', initialLevel: '100',
collateralReleaseLevel: '100', collateralReleaseLevel: '100',
orderMarginLevel: '0',
marginFactor: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
}, },
bestCase: { bestCase: {
maintenanceLevel: '100', maintenanceLevel: '100',
searchLevel: '100', searchLevel: '100',
initialLevel: '100', initialLevel: '100',
collateralReleaseLevel: '100', collateralReleaseLevel: '100',
orderMarginLevel: '0',
marginFactor: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
}, },
}, },
collateralIncreaseEstimate: {
bestCase: '0',
worstCase: '0',
},
liquidation: { liquidation: {
worstCase: { worstCase: {
open_volume_only: worstCaseOpenVolume, open_volume_only: worstCaseOpenVolume,

View File

@ -1,47 +1,35 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useEstimatePositionQuery } from './__generated__/Positions'; import {
type EstimatePositionQueryVariables,
useEstimatePositionQuery,
} from './__generated__/Positions';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../use-t'; import { useT } from '../use-t';
import { MarginMode } from '@vegaprotocol/types';
export const LiquidationPrice = ({ export const LiquidationPrice = ({
marketId,
openVolume,
averageEntryPrice,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
marginMode = MarginMode.MARGIN_MODE_CROSS_MARGIN,
marginFactor,
decimalPlaces, decimalPlaces,
className, className,
}: { ...variables
marketId: string; }: Pick<
openVolume: string; EstimatePositionQueryVariables,
averageEntryPrice: string; | 'marketId'
generalAccountBalance: string; | 'openVolume'
marginAccountBalance: string; | 'orderMarginAccountBalance'
orderMarginAccountBalance: string; | 'generalAccountBalance'
marginMode: MarginMode; | 'averageEntryPrice'
marginFactor: string; | 'marginAccountBalance'
| 'marginMode'
| 'marginFactor'
> & {
decimalPlaces: number; decimalPlaces: number;
className?: string; className?: string;
}) => { }) => {
const t = useT(); const t = useT();
const { data: currentData, previousData } = useEstimatePositionQuery({ const { data: currentData, previousData } = useEstimatePositionQuery({
variables: { variables,
marketId,
openVolume,
averageEntryPrice,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
marginMode,
marginFactor,
},
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
skip: !openVolume || openVolume === '0', skip: !variables.openVolume || variables.openVolume === '0',
}); });
const data = currentData || previousData; const data = currentData || previousData;

View File

@ -52,7 +52,7 @@ export interface Position {
quantum: string; quantum: string;
lossSocializationAmount: string; lossSocializationAmount: string;
marginAccountBalance: string; marginAccountBalance: string;
orderAccountBalance: string; orderMarginAccountBalance: string;
generalAccountBalance: string; generalAccountBalance: string;
marketDecimalPlaces: number; marketDecimalPlaces: number;
marketId: string; marketId: string;
@ -67,6 +67,7 @@ export interface Position {
realisedPNL: string; realisedPNL: string;
status: PositionStatus; status: PositionStatus;
totalBalance: string; totalBalance: string;
totalMarginAccountBalance: string;
unrealisedPNL: string; unrealisedPNL: string;
updatedAt: string | null; updatedAt: string | null;
productType: ProductType; productType: ProductType;
@ -119,7 +120,7 @@ export const getMetrics = (
marginAccount?.balance ?? 0, marginAccount?.balance ?? 0,
asset.decimals asset.decimals
); );
const orderAccountBalance = toBigNum( const orderMarginAccountBalance = toBigNum(
orderAccount?.balance ?? 0, orderAccount?.balance ?? 0,
asset.decimals asset.decimals
); );
@ -137,12 +138,14 @@ export const getMetrics = (
: openVolume.multipliedBy(-1) : openVolume.multipliedBy(-1)
).multipliedBy(markPrice) ).multipliedBy(markPrice)
: undefined; : undefined;
const totalBalance = marginAccountBalance const totalMarginAccountBalance = marginAccountBalance.plus(
.plus(generalAccountBalance) orderMarginAccountBalance
.plus(orderAccountBalance); );
const totalBalance = totalMarginAccountBalance.plus(generalAccountBalance);
const marginMode = const marginMode =
margin?.marginMode || MarginMode.MARGIN_MODE_CROSS_MARGIN; margin?.marginMode || MarginMode.MARGIN_MODE_CROSS_MARGIN;
const marginFactor = margin?.marginFactor; const marginFactor = margin?.marginFactor || '1';
const currentLeverage = const currentLeverage =
marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN
? (marginFactor && 1 / Number(marginFactor)) || undefined ? (marginFactor && 1 / Number(marginFactor)) || undefined
@ -153,7 +156,7 @@ export const getMetrics = (
: undefined; : undefined;
metrics.push({ metrics.push({
marginMode, marginMode,
marginFactor: marginFactor || '0', marginFactor,
maintenanceLevel: margin?.maintenanceLevel, maintenanceLevel: margin?.maintenanceLevel,
assetId: asset.id, assetId: asset.id,
assetSymbol: asset.symbol, assetSymbol: asset.symbol,
@ -163,7 +166,7 @@ export const getMetrics = (
quantum: asset.quantum, quantum: asset.quantum,
lossSocializationAmount: position.lossSocializationAmount || '0', lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0', marginAccountBalance: marginAccount?.balance ?? '0',
orderAccountBalance: orderAccount?.balance ?? '0', orderMarginAccountBalance: orderAccount?.balance ?? '0',
generalAccountBalance: generalAccount?.balance ?? '0', generalAccountBalance: generalAccount?.balance ?? '0',
marketDecimalPlaces, marketDecimalPlaces,
marketId: market.id, marketId: market.id,
@ -180,6 +183,9 @@ export const getMetrics = (
realisedPNL: position.realisedPNL, realisedPNL: position.realisedPNL,
status: position.positionStatus, status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(), totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(),
totalMarginAccountBalance: totalMarginAccountBalance
.multipliedBy(10 ** asset.decimals)
.toFixed(),
unrealisedPNL: position.unrealisedPNL, unrealisedPNL: position.unrealisedPNL,
updatedAt: position.updatedAt || null, updatedAt: position.updatedAt || null,
productType: market?.tradableInstrument.instrument.product productType: market?.tradableInstrument.instrument.product
@ -269,13 +275,26 @@ const positionDataProvider = makeDerivedDataProvider<
} }
); );
export type OpenVolumeData = Pick<
PositionFieldsFragment,
'openVolume' | 'averageEntryPrice'
>;
export const openVolumeDataProvider = makeDerivedDataProvider< export const openVolumeDataProvider = makeDerivedDataProvider<
string, OpenVolumeData,
never, never,
PositionsQueryVariables & MarketDataQueryVariables PositionsQueryVariables & MarketDataQueryVariables
>( >([positionDataProvider], ([data], variables, previousData) =>
[positionDataProvider], produce(previousData, (draft) => {
(data) => (data[0] as PositionFieldsFragment | null)?.openVolume || null if (!data) {
return data;
}
const newData = {
openVolume: (data as PositionFieldsFragment).openVolume,
averageEntryPrice: (data as PositionFieldsFragment).averageEntryPrice,
};
return draft ? Object.assign(draft, newData) : newData;
})
); );
export const rejoinPositionData = ( export const rejoinPositionData = (
@ -376,6 +395,31 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
}) })
); );
const getMaxLeverage = (market: MarketInfo | null) => {
if (!market || !market?.riskFactors) {
return 1;
}
const maxLeverage =
1 /
(Math.max(
Number(market.riskFactors.long),
Number(market.riskFactors.short)
) || 1);
return maxLeverage;
};
export const maxMarketLeverageProvider = makeDerivedDataProvider<
number,
never,
{ marketId: string }
>(
[
(callback, client, { marketId }) =>
marketInfoProvider(callback, client, { marketId }),
],
(parts) => getMaxLeverage(parts[0])
);
export const maxLeverageProvider = makeDerivedDataProvider< export const maxLeverageProvider = makeDerivedDataProvider<
number, number,
never, never,
@ -392,15 +436,7 @@ export const maxLeverageProvider = makeDerivedDataProvider<
const market: MarketInfo | null = parts[0]; const market: MarketInfo | null = parts[0];
const position: PositionFieldsFragment | null = parts[1]; const position: PositionFieldsFragment | null = parts[1];
const margin: MarginFieldsFragment | null = parts[2]; const margin: MarginFieldsFragment | null = parts[2];
if (!market || !market?.riskFactors) { const maxLeverage = getMaxLeverage(market);
return 1;
}
const maxLeverage =
1 /
(Math.max(
Number(market.riskFactors.long),
Number(market.riskFactors.short)
) || 1);
if ( if (
market && market &&
@ -432,10 +468,9 @@ export const maxLeverageProvider = makeDerivedDataProvider<
} }
); );
export const useMaxLeverage = (marketId: string, partyId?: string) => { export const useMaxLeverage = (marketId: string, partyId: string | null) => {
return useDataProvider({ return useDataProvider({
dataProvider: maxLeverageProvider, dataProvider: partyId ? maxLeverageProvider : maxMarketLeverageProvider,
variables: { marketId, partyId: partyId || '' }, variables: { marketId, partyId: partyId || '' },
skip: !partyId,
}); });
}; };

View File

@ -137,58 +137,61 @@ const PositionMargin = ({ data }: { data: Position }) => {
? ( ? (
BigInt(data.marginAccountBalance) + BigInt(data.generalAccountBalance) BigInt(data.marginAccountBalance) + BigInt(data.generalAccountBalance)
).toString() ).toString()
: BigInt(data.marginAccountBalance) > BigInt(data.orderAccountBalance) : BigInt(data.marginAccountBalance) >
BigInt(data.orderMarginAccountBalance)
? data.marginAccountBalance ? data.marginAccountBalance
: data.orderAccountBalance; : data.orderMarginAccountBalance;
const getWidth = (balance: string) => const getWidth = (balance: string) =>
BigNumber(balance).multipliedBy(100).dividedBy(max).toNumber(); BigNumber(balance).multipliedBy(100).dividedBy(max).toNumber();
const inCrossMode = data.marginMode === MarginMode.MARGIN_MODE_CROSS_MARGIN; const inCrossMode = data.marginMode === MarginMode.MARGIN_MODE_CROSS_MARGIN;
const hasOrderAccountBalance = const hasOrderMarginAccountBalance =
!inCrossMode && data.orderAccountBalance !== '0'; !inCrossMode && data.orderMarginAccountBalance !== '0';
return ( return (
<> <>
<MarginChart {data.marginAccountBalance !== '0' && (
width={inCrossMode ? getWidth(data.marginAccountBalance) : undefined} <MarginChart
label={t('Margin: {{balance}}', { width={inCrossMode ? getWidth(data.marginAccountBalance) : undefined}
balance: addDecimalsFormatNumberQuantum( label={t('Margin: {{balance}}', {
data.marginAccountBalance, balance: addDecimalsFormatNumberQuantum(
data.assetDecimals, data.marginAccountBalance,
data.quantum
),
})}
other={
inCrossMode
? t('General account: {{balance}}', {
balance: addDecimalsFormatNumberQuantum(
data.generalAccountBalance,
data.assetDecimals,
data.quantum
),
})
: undefined
}
className={classnames({ 'mb-2': hasOrderAccountBalance })}
marker={
data.maintenanceLevel ? getWidth(data.maintenanceLevel) : undefined
}
markerLabel={
data.maintenanceLevel &&
t('Liquidation: {{maintenanceLevel}}', {
maintenanceLevel: addDecimalsFormatNumberQuantum(
data.maintenanceLevel,
data.assetDecimals, data.assetDecimals,
data.quantum data.quantum
), ),
}) })}
} other={
/> inCrossMode
{hasOrderAccountBalance ? ( ? t('General account: {{balance}}', {
balance: addDecimalsFormatNumberQuantum(
data.generalAccountBalance,
data.assetDecimals,
data.quantum
),
})
: undefined
}
className={classnames({ 'mb-2': hasOrderMarginAccountBalance })}
marker={
data.maintenanceLevel ? getWidth(data.maintenanceLevel) : undefined
}
markerLabel={
data.maintenanceLevel &&
t('Liquidation: {{maintenanceLevel}}', {
maintenanceLevel: addDecimalsFormatNumberQuantum(
data.maintenanceLevel,
data.assetDecimals,
data.quantum
),
})
}
/>
)}
{hasOrderMarginAccountBalance ? (
<MarginChart <MarginChart
width={getWidth(data.orderAccountBalance)} width={getWidth(data.orderMarginAccountBalance)}
label={t('Order: {{balance}}', { label={t('Order: {{balance}}', {
balance: addDecimalsFormatNumber( balance: addDecimalsFormatNumber(
data.orderAccountBalance, data.orderMarginAccountBalance,
data.assetDecimals data.assetDecimals
), ),
})} })}
@ -340,20 +343,16 @@ export const PositionsTable = ({
return !data return !data
? undefined ? undefined
: toBigNum( : toBigNum(
data.marginAccountBalance, data.totalMarginAccountBalance,
data.assetDecimals data.assetDecimals
).toNumber(); ).toNumber();
}, },
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => { cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if ( if (!data || !data.totalMarginAccountBalance) {
!data ||
!data.marginAccountBalance ||
!data.marketDecimalPlaces
) {
return null; return null;
} }
const margin = addDecimalsFormatNumberQuantum( const margin = addDecimalsFormatNumberQuantum(
data.marginAccountBalance, data.totalMarginAccountBalance,
data.assetDecimals, data.assetDecimals,
data.quantum data.quantum
); );
@ -364,7 +363,7 @@ export const PositionsTable = ({
<Tooltip <Tooltip
description={ description={
data && data &&
data.marginAccountBalance !== '0' && ( data.totalMarginAccountBalance !== '0' && (
<PositionMargin data={data} /> <PositionMargin data={data} />
) )
} }
@ -410,10 +409,10 @@ export const PositionsTable = ({
className="block text-right grow" className="block text-right grow"
marketId={data.marketId} marketId={data.marketId}
openVolume={data.openVolume} openVolume={data.openVolume}
averageEntryPrice={data.averageEntryPrice}
generalAccountBalance={data.generalAccountBalance} generalAccountBalance={data.generalAccountBalance}
marginAccountBalance={data.marginAccountBalance} marginAccountBalance={data.marginAccountBalance}
orderMarginAccountBalance={data.orderAccountBalance} orderMarginAccountBalance={data.orderMarginAccountBalance}
averageEntryPrice={data.averageEntryPrice}
marginFactor={data.marginFactor} marginFactor={data.marginFactor}
marginMode={data.marginMode} marginMode={data.marginMode}
decimalPlaces={data.marketDecimalPlaces} decimalPlaces={data.marketDecimalPlaces}

View File

@ -181,11 +181,11 @@ const marginsFields: MarginFieldsFragment[] = [
]; ];
export const singleRow: Position = { export const singleRow: Position = {
marginFactor: '1',
generalAccountBalance: '12345600', generalAccountBalance: '12345600',
maintenanceLevel: '12300000', maintenanceLevel: '12300000',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
marginFactor: '1', orderMarginAccountBalance: '0',
orderAccountBalance: '0',
partyId: 'partyId', partyId: 'partyId',
assetId: 'asset-id', assetId: 'asset-id',
assetSymbol: 'BTC', assetSymbol: 'BTC',
@ -195,6 +195,7 @@ export const singleRow: Position = {
quantum: '0.1', quantum: '0.1',
lossSocializationAmount: '0', lossSocializationAmount: '0',
marginAccountBalance: '12345600', marginAccountBalance: '12345600',
totalMarginAccountBalance: '12345600',
marketDecimalPlaces: 1, marketDecimalPlaces: 1,
marketId: 'string', marketId: 'string',
marketCode: 'ETHBTC.QM21', marketCode: 'ETHBTC.QM21',

View File

@ -1,14 +1,17 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { openVolumeDataProvider } from './positions-data-providers'; import {
OpenVolumeData,
openVolumeDataProvider,
} from './positions-data-providers';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
export const useOpenVolume = ( export const useOpenVolume = (
partyId: string | null | undefined, partyId: string | null | undefined,
marketId: string marketId: string
) => { ) => {
const [openVolume, setOpenVolume] = useState<string | undefined>(undefined); const [openVolume, setOpenVolume] = useState<OpenVolumeData | null>(null);
const update = useCallback(({ data }: { data: string | null }) => { const update = useCallback(({ data }: { data: OpenVolumeData | null }) => {
setOpenVolume(data ?? undefined); setOpenVolume(data);
return true; return true;
}, []); }, []);
useDataProvider({ useDataProvider({

View File

@ -5,7 +5,9 @@ import classNames from 'classnames';
export const LeverageSlider = ( export const LeverageSlider = (
props: Omit<SliderProps, 'min' | 'max'> & Required<Pick<SliderProps, 'max'>> props: Omit<SliderProps, 'min' | 'max'> & Required<Pick<SliderProps, 'max'>>
) => { ) => {
const step = [2, 5, 10, 20, 25].find((step) => props.max / step <= 6); const step = [2, 5, 10, 20, 25, 50, 100].find(
(step) => props.max / step <= 6
);
const min = 1; const min = 1;
const value = props.value?.[0] || props.defaultValue?.[0]; const value = props.value?.[0] || props.defaultValue?.[0];
return ( return (
@ -28,6 +30,7 @@ export const LeverageSlider = (
const higherThanValue = value && labelValue > value; const higherThanValue = value && labelValue > value;
return ( return (
<span <span
key={labelValue}
className="absolute flex flex-col items-center translate-x-[-50%]" className="absolute flex flex-col items-center translate-x-[-50%]"
style={{ style={{
left: `${ left: `${