Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed7b1a7034 | ||
|
|
a1381ed68a | ||
|
|
e2763cc738 | ||
|
|
a49139f127 | ||
|
|
e216b23472 | ||
|
|
e52ae97233 | ||
|
|
1780f6fa7f |
@@ -32,6 +32,7 @@ import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
|
||||
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
||||
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -133,6 +134,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsApplyReferralCode;
|
||||
case 'Join Team':
|
||||
return TxDetailsJoinTeam;
|
||||
case 'Update Margin Mode':
|
||||
return TxDetailsUpdateMarginMode;
|
||||
default:
|
||||
return TxDetailsGeneric;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { MarketLink } from '../../links';
|
||||
|
||||
interface TxDetailsUpdateMarginModeProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
type Mode = components['schemas']['UpdateMarginModeMode'];
|
||||
|
||||
const MarginModeLabels: Record<Mode, string> = {
|
||||
MODE_CROSS_MARGIN: t('Cross margin'),
|
||||
MODE_ISOLATED_MARGIN: t('Isolated margin'),
|
||||
MODE_UNSPECIFIED: t('Unspecified'),
|
||||
};
|
||||
|
||||
export const TxDetailsUpdateMarginMode = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsUpdateMarginModeProps) => {
|
||||
if (!txData || !txData.command.updateMarginMode) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const u: components['schemas']['v1UpdateMarginMode'] =
|
||||
txData.command.updateMarginMode;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
{u.marketId && (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Market ID')}</TableCell>
|
||||
<TableCell>
|
||||
<MarketLink id={u.marketId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{u.mode && (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('New margin mode')}</TableCell>
|
||||
<TableCell>{MarginModeLabels[u.mode]}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{u.marginFactor && (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Margin factor')}</TableCell>
|
||||
<TableCell>{u.marginFactor}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -44,6 +44,7 @@ export type FilterOption =
|
||||
| 'Transfer Funds'
|
||||
| 'Undelegate'
|
||||
| 'Update Referral Set'
|
||||
| 'Update Margin Mode'
|
||||
| 'Validator Heartbeat'
|
||||
| 'Vote on Proposal'
|
||||
| 'Withdraw';
|
||||
@@ -59,6 +60,7 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
||||
'Stop Orders Submission',
|
||||
'Stop Orders Cancellation',
|
||||
'Submit Order',
|
||||
'Update Margin Mode',
|
||||
],
|
||||
'Transfers and Withdrawals': [
|
||||
'Transfer Funds',
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CompetitionsCreateTeam = () => {
|
||||
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||
<Box className="flex flex-col gap-4">
|
||||
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
|
||||
{t('Create a team')}
|
||||
{isSolo ? t('Create solo team') : t('Create a team')}
|
||||
</h1>
|
||||
{pubKey && !isReadOnly ? (
|
||||
<CreateTeamFormContainer isSolo={isSolo} />
|
||||
|
||||
@@ -100,8 +100,10 @@ const TeamPage = ({
|
||||
>
|
||||
{team.name}
|
||||
</h1>
|
||||
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||
<UpdateTeamButton team={team} />
|
||||
<div className="flex gap-2">
|
||||
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||
<UpdateTeamButton team={team} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<TeamStats stats={stats} members={members} games={games} />
|
||||
|
||||
@@ -97,6 +97,7 @@ export const TeamForm = ({
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
private: isSolo,
|
||||
allowList: '',
|
||||
...defaultValues,
|
||||
},
|
||||
});
|
||||
@@ -160,59 +161,67 @@ export const TeamForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
disabled={isSolo}
|
||||
{!isSolo && (
|
||||
<>
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
disabled={isSolo}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{isPrivate && (
|
||||
<TradingFormGroup
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
disabled: isSolo,
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{isPrivate && (
|
||||
<TradingFormGroup
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
)}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
disabled: isSolo,
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
</>
|
||||
)}
|
||||
{err && (
|
||||
<p className="text-danger text-xs mb-4 first-letter:capitalize">
|
||||
{err}
|
||||
</p>
|
||||
)}
|
||||
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
|
||||
<SubmitButton type={type} status={status} />
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,10 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
||||
const t = useT();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
|
||||
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||
@@ -12,7 +14,9 @@ export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
||||
data-testid="update-team-button"
|
||||
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||
intent={Intent.Info}
|
||||
/>
|
||||
>
|
||||
{t('Update team')}
|
||||
</TradingAnchorButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_switch_cross_isolated_margin(
|
||||
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")
|
||||
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
|
||||
page.wait_for_timeout(1000)
|
||||
page.get_by_test_id(tab_positions).get_by_text("Isolated").hover()
|
||||
|
||||
@@ -23,7 +23,6 @@ def continuous_market(vega):
|
||||
return setup_continuous_market(vega)
|
||||
|
||||
|
||||
@pytest.mark.skip("marked id issue #5681")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
|
||||
@@ -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;
|
||||
@@ -6,8 +6,7 @@ export * from './accounts-manager';
|
||||
export * from './breakdown-table';
|
||||
export * from './use-account-balance';
|
||||
export * from './get-settlement-account';
|
||||
export * from './use-market-account-balance';
|
||||
export * from './use-margin-account-balance';
|
||||
export * from './__generated__/Margins';
|
||||
export { MarginHealthChart } from './margin-health-chart';
|
||||
export * from './margin-data-provider';
|
||||
export * from './transfer-container';
|
||||
|
||||
@@ -83,3 +83,25 @@ export const marketMarginDataProvider = makeDerivedDataProvider<
|
||||
(margin) => margin.market.id === marketId
|
||||
) || 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;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
};
|
||||
@@ -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]
|
||||
);
|
||||
};
|
||||
@@ -1,47 +1,14 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { getAsset } from '@vegaprotocol/markets';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
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 { formatNumberPercentage, formatValue } from '@vegaprotocol/utils';
|
||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
Intent,
|
||||
ExternalLink,
|
||||
Pill,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { Intent, Pill } from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { FeesBreakdown } from '../fees-breakdown';
|
||||
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
|
||||
import { useT, ns } from '../../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,12 +26,25 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import { useT, ns } from '../../use-t';
|
||||
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 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 = ({
|
||||
marginAccountBalance,
|
||||
generalAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
assetSymbol,
|
||||
market,
|
||||
onMarketClick,
|
||||
@@ -48,31 +61,44 @@ export const DealTicketMarginDetails = ({
|
||||
});
|
||||
const liquidationEstimate = positionEstimate?.liquidation;
|
||||
const marginEstimate = positionEstimate?.margin;
|
||||
const totalMarginAccountBalance =
|
||||
BigInt(marginAccountBalance || '0') +
|
||||
BigInt(orderMarginAccountBalance || '0');
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
|
||||
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
let marginRequiredBestCase: 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 (currentMargins) {
|
||||
const currentMargin =
|
||||
BigInt(currentMargins.initialLevel) +
|
||||
BigInt(currentMargins.orderMarginLevel);
|
||||
|
||||
marginRequiredBestCase = (
|
||||
BigInt(marginEstimate.bestCase.initialLevel) -
|
||||
BigInt(currentMargins.initialLevel)
|
||||
marginEstimateBestCase - currentMargin
|
||||
).toString();
|
||||
if (marginRequiredBestCase.startsWith('-')) {
|
||||
marginRequiredBestCase = '0';
|
||||
}
|
||||
|
||||
marginRequiredWorstCase = (
|
||||
BigInt(marginEstimate.worstCase.initialLevel) -
|
||||
BigInt(currentMargins.initialLevel)
|
||||
marginEstimateWorstCase - currentMargin
|
||||
).toString();
|
||||
|
||||
if (marginRequiredWorstCase.startsWith('-')) {
|
||||
marginRequiredWorstCase = '0';
|
||||
}
|
||||
} else {
|
||||
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
|
||||
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
|
||||
marginRequiredBestCase = marginEstimateBestCase.toString();
|
||||
marginRequiredWorstCase = marginEstimateWorstCase.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +110,12 @@ export const DealTicketMarginDetails = ({
|
||||
|
||||
let deductionFromCollateral = null;
|
||||
let projectedMargin = null;
|
||||
if (marginAccountBalance) {
|
||||
if (totalMarginAccountBalance) {
|
||||
const deductionFromCollateralBestCase =
|
||||
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
|
||||
BigInt(marginAccountBalance);
|
||||
marginEstimateBestCase - totalMarginAccountBalance;
|
||||
|
||||
const deductionFromCollateralWorstCase =
|
||||
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
|
||||
BigInt(marginAccountBalance);
|
||||
marginEstimateWorstCase - totalMarginAccountBalance;
|
||||
|
||||
deductionFromCollateral = (
|
||||
<KeyValue
|
||||
@@ -125,12 +149,12 @@ export const DealTicketMarginDetails = ({
|
||||
<KeyValue
|
||||
label={t('Projected margin')}
|
||||
value={formatRange(
|
||||
marginEstimate?.bestCase.initialLevel,
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
marginEstimateBestCase.toString(),
|
||||
marginEstimateWorstCase.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
marginEstimateWorstCase.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
@@ -276,6 +300,11 @@ export const DealTicketMarginDetails = ({
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
orderMarginAccountBalance: formatValue(
|
||||
orderMarginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginMaintenance: formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
@@ -294,14 +323,17 @@ export const DealTicketMarginDetails = ({
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
value={formatValue(
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
marginAccountBalance,
|
||||
totalMarginAccountBalance.toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
|
||||
@@ -58,8 +58,9 @@ import type {
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
import {
|
||||
useMarketAccountBalance,
|
||||
useMarginAccountBalance,
|
||||
useAccountBalance,
|
||||
marginModeDataProvider,
|
||||
} from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { type OrderFormValues } from '../../hooks';
|
||||
@@ -166,9 +167,10 @@ export const DealTicket = ({
|
||||
|
||||
const asset = getAsset(market);
|
||||
const {
|
||||
accountBalance: marginAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
marginAccountBalance,
|
||||
loading: loadingMarginAccountBalance,
|
||||
} = useMarketAccountBalance(market.id);
|
||||
} = useMarginAccountBalance(market.id);
|
||||
|
||||
const {
|
||||
accountBalance: generalAccountBalance,
|
||||
@@ -176,7 +178,9 @@ export const DealTicket = ({
|
||||
} = useAccountBalance(asset.id);
|
||||
|
||||
const balance = (
|
||||
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
||||
BigInt(marginAccountBalance) +
|
||||
BigInt(generalAccountBalance) +
|
||||
BigInt(orderMarginAccountBalance)
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
@@ -241,7 +245,19 @@ export const DealTicket = ({
|
||||
variables: { partyId: pubKey || '', marketId: market.id },
|
||||
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
|
||||
? activeOrders.map<Schema.OrderInfo>((order) => ({
|
||||
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
|
||||
@@ -259,21 +275,25 @@ export const DealTicket = ({
|
||||
});
|
||||
}
|
||||
|
||||
const positionEstimate = usePositionEstimate({
|
||||
marketId: market.id,
|
||||
openVolume,
|
||||
orders,
|
||||
marginAccountBalance: marginAccountBalance,
|
||||
generalAccountBalance: generalAccountBalance,
|
||||
orderMarginAccountBalance: '0', // TODO: Get real balance
|
||||
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, // TODO: unhardcode this and get users margin mode for the market
|
||||
averageEntryPrice: marketPrice || '0', // TODO: This assumes the order will be entirely filled at the current market price
|
||||
skip:
|
||||
!normalizedOrder ||
|
||||
const positionEstimate = usePositionEstimate(
|
||||
{
|
||||
marketId: market.id,
|
||||
openVolume,
|
||||
averageEntryPrice,
|
||||
orders,
|
||||
marginAccountBalance: marginAccountBalance || '0',
|
||||
generalAccountBalance: generalAccountBalance || '0',
|
||||
orderMarginAccountBalance: orderMarginAccountBalance || '0',
|
||||
marginFactor: margin?.marginFactor || '1',
|
||||
marginMode:
|
||||
margin?.marginMode || Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
includeCollateralIncreaseInAvailableCollateral: true,
|
||||
},
|
||||
!normalizedOrder ||
|
||||
(normalizedOrder.type !== Schema.OrderType.TYPE_MARKET &&
|
||||
(!normalizedOrder.price || normalizedOrder.price === '0')) ||
|
||||
normalizedOrder.size === '0',
|
||||
});
|
||||
normalizedOrder.size === '0'
|
||||
);
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
@@ -319,7 +339,9 @@ export const DealTicket = ({
|
||||
}
|
||||
|
||||
const hasNoBalance =
|
||||
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
||||
!BigInt(generalAccountBalance) &&
|
||||
!BigInt(marginAccountBalance) &&
|
||||
!BigInt(orderMarginAccountBalance);
|
||||
if (
|
||||
hasNoBalance &&
|
||||
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||
@@ -349,6 +371,7 @@ export const DealTicket = ({
|
||||
marketTradingMode,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
loadingMarginAccountBalance,
|
||||
loadingGeneralAccountBalance,
|
||||
pubKey,
|
||||
@@ -707,10 +730,16 @@ export const DealTicket = ({
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
margin={
|
||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
||||
'0'
|
||||
}
|
||||
margin={(
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
||||
'0'
|
||||
) +
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase
|
||||
.orderMarginLevel || '0'
|
||||
)
|
||||
).toString()}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onDeposit={onDeposit}
|
||||
@@ -743,6 +772,7 @@ export const DealTicket = ({
|
||||
onMarketClick={onMarketClick}
|
||||
assetSymbol={asset.symbol}
|
||||
marginAccountBalance={marginAccountBalance}
|
||||
orderMarginAccountBalance={orderMarginAccountBalance}
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
positionEstimate={positionEstimate?.estimatePosition}
|
||||
market={market}
|
||||
@@ -768,8 +798,20 @@ interface SummaryMessageProps {
|
||||
|
||||
export const NoWalletWarning = ({
|
||||
isReadOnly,
|
||||
}: Pick<SummaryMessageProps, 'isReadOnly'>) => {
|
||||
noWalletConnected,
|
||||
}: Pick<SummaryMessageProps, 'isReadOnly'> & {
|
||||
noWalletConnected?: boolean;
|
||||
}) => {
|
||||
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) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
TradingButton as Button,
|
||||
TradingInput as Input,
|
||||
FormGroup,
|
||||
LeverageSlider,
|
||||
Notification,
|
||||
Intent,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
@@ -15,15 +18,151 @@ import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useT } from '../../use-t';
|
||||
import classnames from 'classnames';
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useMaxLeverage } from '@vegaprotocol/positions';
|
||||
import {
|
||||
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;
|
||||
|
||||
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 {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
marketId: string;
|
||||
partyId: string;
|
||||
create: VegaTransactionStore['create'];
|
||||
}
|
||||
|
||||
@@ -33,6 +172,7 @@ const CrossMarginModeDialog = ({
|
||||
marketId,
|
||||
create,
|
||||
}: MarginDialogProps) => {
|
||||
const { pubKey: partyId, isReadOnly } = useVegaWallet();
|
||||
const t = useT();
|
||||
return (
|
||||
<Dialog
|
||||
@@ -60,15 +200,24 @@ const CrossMarginModeDialog = ({
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<MarginChange
|
||||
marketId={marketId}
|
||||
partyId={partyId}
|
||||
marginMode={Types.MarginMode.MARGIN_MODE_CROSS_MARGIN}
|
||||
marginFactor="1"
|
||||
/>
|
||||
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
create({
|
||||
updateMarginMode: {
|
||||
marketId,
|
||||
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
});
|
||||
partyId &&
|
||||
!isReadOnly &&
|
||||
create({
|
||||
updateMarginMode: {
|
||||
marketId,
|
||||
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
@@ -82,10 +231,10 @@ const IsolatedMarginModeDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
marketId,
|
||||
partyId,
|
||||
marginFactor,
|
||||
create,
|
||||
}: MarginDialogProps & { marginFactor: string }) => {
|
||||
const { pubKey: partyId, isReadOnly } = useVegaWallet();
|
||||
const [leverage, setLeverage] = useState(
|
||||
Number((1 / Number(marginFactor)).toFixed(1))
|
||||
);
|
||||
@@ -129,13 +278,15 @@ const IsolatedMarginModeDialog = ({
|
||||
</div>
|
||||
<form
|
||||
onSubmit={() => {
|
||||
create({
|
||||
updateMarginMode: {
|
||||
marketId,
|
||||
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
|
||||
marginFactor: `${1 / leverage}`,
|
||||
},
|
||||
});
|
||||
partyId &&
|
||||
!isReadOnly &&
|
||||
create({
|
||||
updateMarginMode: {
|
||||
marketId,
|
||||
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
|
||||
marginFactor: `${1 / leverage}`,
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
@@ -144,7 +295,7 @@ const IsolatedMarginModeDialog = ({
|
||||
<LeverageSlider
|
||||
max={max}
|
||||
step={0.1}
|
||||
value={[leverage]}
|
||||
value={[leverage || 1]}
|
||||
onValueChange={([value]) => setLeverage(value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -154,10 +305,17 @@ const IsolatedMarginModeDialog = ({
|
||||
min={1}
|
||||
max={max}
|
||||
step={0.1}
|
||||
value={leverage}
|
||||
value={leverage || ''}
|
||||
onChange={(e) => setLeverage(Number(e.target.value))}
|
||||
/>
|
||||
</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">
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
@@ -169,27 +327,21 @@ const IsolatedMarginModeDialog = ({
|
||||
export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
|
||||
const t = useT();
|
||||
const [dialog, setDialog] = useState<'cross' | 'isolated' | ''>();
|
||||
const { pubKey: partyId, isReadOnly } = useVegaWallet();
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: margin } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
dataProvider: marginModeDataProvider,
|
||||
variables: {
|
||||
partyId: partyId || '',
|
||||
marketId,
|
||||
},
|
||||
skip: !partyId,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!partyId) {
|
||||
setDialog('');
|
||||
}
|
||||
}, [partyId]);
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const marginMode = margin?.marginMode;
|
||||
const marginFactor =
|
||||
margin?.marginFactor && margin?.marginFactor !== '0'
|
||||
? margin?.marginFactor
|
||||
: undefined;
|
||||
const disabled = isReadOnly;
|
||||
const onClose = () => setDialog(undefined);
|
||||
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">
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={() => partyId && setDialog('cross')}
|
||||
type="button"
|
||||
onClick={() => setDialog('cross')}
|
||||
className={classnames('rounded', {
|
||||
[enabledModeClassName]:
|
||||
!marginMode ||
|
||||
@@ -208,8 +360,8 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
|
||||
{t('Cross')}
|
||||
</button>
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={() => partyId && setDialog('isolated')}
|
||||
type="button"
|
||||
onClick={() => setDialog('isolated')}
|
||||
className={classnames('rounded', {
|
||||
[enabledModeClassName]:
|
||||
marginMode === Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
|
||||
@@ -222,25 +374,23 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
{partyId && (
|
||||
{
|
||||
<CrossMarginModeDialog
|
||||
partyId={partyId}
|
||||
open={dialog === 'cross'}
|
||||
onClose={onClose}
|
||||
marketId={marketId}
|
||||
create={create}
|
||||
/>
|
||||
)}
|
||||
{partyId && (
|
||||
}
|
||||
{
|
||||
<IsolatedMarginModeDialog
|
||||
partyId={partyId}
|
||||
open={dialog === 'isolated'}
|
||||
onClose={onClose}
|
||||
marketId={marketId}
|
||||
create={create}
|
||||
marginFactor={marginFactor || `${1 / defaultLeverage}`}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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.';
|
||||
|
||||
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 =
|
||||
'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.';
|
||||
|
||||
@@ -5,37 +5,15 @@ import {
|
||||
import { useEstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface PositionEstimateProps extends EstimatePositionQueryVariables {
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
export const usePositionEstimate = ({
|
||||
marketId,
|
||||
openVolume,
|
||||
orders,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
averageEntryPrice,
|
||||
marginMode,
|
||||
marginFactor,
|
||||
skip,
|
||||
}: PositionEstimateProps) => {
|
||||
export const usePositionEstimate = (
|
||||
variables: EstimatePositionQueryVariables,
|
||||
skip: boolean
|
||||
) => {
|
||||
const [estimates, setEstimates] = useState<EstimatePositionQuery | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { data } = useEstimatePositionQuery({
|
||||
variables: {
|
||||
marketId,
|
||||
openVolume,
|
||||
orders,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
averageEntryPrice,
|
||||
marginMode,
|
||||
marginFactor,
|
||||
},
|
||||
variables,
|
||||
skip,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
"below": "below",
|
||||
"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",
|
||||
"Closing on {{time}}": "Closing on {{time}}",
|
||||
"Confirm": "Confirm",
|
||||
@@ -67,6 +69,13 @@
|
||||
"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 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 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}}",
|
||||
@@ -124,7 +133,7 @@
|
||||
"Total": "Total",
|
||||
"Total fees": "Total fees",
|
||||
"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",
|
||||
"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}}",
|
||||
@@ -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 isolated margin mode.": "You are setting this market to isolated margin mode.",
|
||||
"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 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 price": "You need provide a price",
|
||||
"You need provide a trailing percent offset": "You need provide a trailing percent offset",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Create": "Create",
|
||||
"Create a team": "Create a team",
|
||||
"Create a simple referral code to enjoy the referrer commission outlined in the current referral program": "Create a simple referral code to enjoy the referrer commission outlined in the current referral program",
|
||||
"Create solo team": "Create solo team",
|
||||
"Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards",
|
||||
"Current tier": "Current tier",
|
||||
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
|
||||
@@ -363,11 +364,13 @@
|
||||
"Type": "Type",
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
"Update team":, "Update team",
|
||||
"URL": "URL",
|
||||
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
|
||||
"Vega chart": "Vega chart",
|
||||
"Vega Reward pot": "Vega Reward pot",
|
||||
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
|
||||
"Vega Wallet <0>full featured</0>": "Vega Wallet <0>full featured</0>",
|
||||
"Vega chart": "Vega chart",
|
||||
"Vesting": "Vesting",
|
||||
"Vesting multiplier": "Vesting multiplier",
|
||||
"Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}",
|
||||
|
||||
@@ -113,6 +113,8 @@ export const filterAndSortClosedMarkets = (markets: MarketMaybeWithData[]) => {
|
||||
return [
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_CLOSED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
].includes(m.data?.marketState || m.state);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -41,24 +41,26 @@ subscription PositionsSubscription($partyId: ID!) {
|
||||
query EstimatePosition(
|
||||
$marketId: ID!
|
||||
$openVolume: String!
|
||||
$orders: [OrderInfo!]
|
||||
$averageEntryPrice: String!
|
||||
$orders: [OrderInfo!]
|
||||
$marginAccountBalance: String!
|
||||
$generalAccountBalance: String!
|
||||
$orderMarginAccountBalance: String!
|
||||
$marginMode: MarginMode!
|
||||
$marginFactor: String
|
||||
$includeCollateralIncreaseInAvailableCollateral: Boolean
|
||||
) {
|
||||
estimatePosition(
|
||||
marketId: $marketId
|
||||
openVolume: $openVolume
|
||||
orders: $orders
|
||||
averageEntryPrice: $averageEntryPrice
|
||||
orders: $orders
|
||||
marginAccountBalance: $marginAccountBalance
|
||||
generalAccountBalance: $generalAccountBalance
|
||||
orderMarginAccountBalance: $orderMarginAccountBalance
|
||||
marginMode: $marginMode
|
||||
marginFactor: $marginFactor
|
||||
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
||||
# 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
|
||||
# query will return a full value requiring formatting using asset.decimals. For consistency
|
||||
@@ -71,14 +73,24 @@ query EstimatePosition(
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
bestCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
}
|
||||
collateralIncreaseEstimate {
|
||||
worstCase
|
||||
bestCase
|
||||
}
|
||||
liquidation {
|
||||
worstCase {
|
||||
open_volume_only
|
||||
|
||||
+18
-5
@@ -22,17 +22,18 @@ export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', p
|
||||
export type EstimatePositionQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
openVolume: Types.Scalars['String'];
|
||||
orders?: Types.InputMaybe<Array<Types.OrderInfo> | Types.OrderInfo>;
|
||||
averageEntryPrice: Types.Scalars['String'];
|
||||
orders?: Types.InputMaybe<Array<Types.OrderInfo> | Types.OrderInfo>;
|
||||
marginAccountBalance: Types.Scalars['String'];
|
||||
generalAccountBalance: Types.Scalars['String'];
|
||||
orderMarginAccountBalance: Types.Scalars['String'];
|
||||
marginMode: Types.MarginMode;
|
||||
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`
|
||||
fragment PositionFields on Position {
|
||||
@@ -129,17 +130,18 @@ export function usePositionsSubscriptionSubscription(baseOptions: Apollo.Subscri
|
||||
export type PositionsSubscriptionSubscriptionHookResult = ReturnType<typeof usePositionsSubscriptionSubscription>;
|
||||
export type PositionsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<PositionsSubscriptionSubscription>;
|
||||
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(
|
||||
marketId: $marketId
|
||||
openVolume: $openVolume
|
||||
orders: $orders
|
||||
averageEntryPrice: $averageEntryPrice
|
||||
orders: $orders
|
||||
marginAccountBalance: $marginAccountBalance
|
||||
generalAccountBalance: $generalAccountBalance
|
||||
orderMarginAccountBalance: $orderMarginAccountBalance
|
||||
marginMode: $marginMode
|
||||
marginFactor: $marginFactor
|
||||
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
|
||||
scaleLiquidationPriceToMarketDecimals: true
|
||||
) {
|
||||
margin {
|
||||
@@ -148,14 +150,24 @@ export const EstimatePositionDocument = gql`
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
bestCase {
|
||||
maintenanceLevel
|
||||
searchLevel
|
||||
initialLevel
|
||||
collateralReleaseLevel
|
||||
marginMode
|
||||
marginFactor
|
||||
orderMarginLevel
|
||||
}
|
||||
}
|
||||
collateralIncreaseEstimate {
|
||||
worstCase
|
||||
bestCase
|
||||
}
|
||||
liquidation {
|
||||
worstCase {
|
||||
open_volume_only
|
||||
@@ -186,13 +198,14 @@ export const EstimatePositionDocument = gql`
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* openVolume: // value for 'openVolume'
|
||||
* orders: // value for 'orders'
|
||||
* averageEntryPrice: // value for 'averageEntryPrice'
|
||||
* orders: // value for 'orders'
|
||||
* marginAccountBalance: // value for 'marginAccountBalance'
|
||||
* generalAccountBalance: // value for 'generalAccountBalance'
|
||||
* orderMarginAccountBalance: // value for 'orderMarginAccountBalance'
|
||||
* marginMode: // value for 'marginMode'
|
||||
* marginFactor: // value for 'marginFactor'
|
||||
* includeCollateralIncreaseInAvailableCollateral: // value for 'includeCollateralIncreaseInAvailableCollateral'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import merge from 'lodash/merge';
|
||||
import type { EstimatePositionQuery } from './__generated__/Positions';
|
||||
import { MarginMode } from '@vegaprotocol/types';
|
||||
|
||||
export const estimatePositionQuery = (
|
||||
override?: PartialDeep<EstimatePositionQuery>
|
||||
@@ -14,14 +15,24 @@ export const estimatePositionQuery = (
|
||||
initialLevel: '500000',
|
||||
maintenanceLevel: '200000',
|
||||
searchLevel: '300000',
|
||||
marginFactor: '1',
|
||||
orderMarginLevel: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
worstCase: {
|
||||
collateralReleaseLevel: '1100000',
|
||||
initialLevel: '600000',
|
||||
maintenanceLevel: '300000',
|
||||
searchLevel: '400000',
|
||||
marginFactor: '1',
|
||||
orderMarginLevel: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
},
|
||||
collateralIncreaseEstimate: {
|
||||
bestCase: '0',
|
||||
worstCase: '0',
|
||||
},
|
||||
liquidation: {
|
||||
bestCase: {
|
||||
including_buy_orders: '1',
|
||||
|
||||
@@ -9,32 +9,23 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { MarginMode } from '@vegaprotocol/types';
|
||||
|
||||
describe('LiquidationPrice', () => {
|
||||
const props = {
|
||||
const variables = {
|
||||
marketId: 'market-id',
|
||||
openVolume: '100',
|
||||
decimalPlaces: 2,
|
||||
averageEntryPrice: '100',
|
||||
generalAccountBalance: '100',
|
||||
marginAccountBalance: '100',
|
||||
orderMarginAccountBalance: '100',
|
||||
averageEntryPrice: '10',
|
||||
marginAccountBalance: '500',
|
||||
generalAccountBalance: '500',
|
||||
orderMarginAccountBalance: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
marginFactor: '1',
|
||||
};
|
||||
const props = { ...variables, decimalPlaces: 2 };
|
||||
const worstCaseOpenVolume = '200';
|
||||
const bestCaseOpenVolume = '100';
|
||||
const mock: MockedResponse<EstimatePositionQuery> = {
|
||||
request: {
|
||||
query: EstimatePositionDocument,
|
||||
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,
|
||||
},
|
||||
variables,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -45,14 +36,24 @@ describe('LiquidationPrice', () => {
|
||||
searchLevel: '100',
|
||||
initialLevel: '100',
|
||||
collateralReleaseLevel: '100',
|
||||
orderMarginLevel: '0',
|
||||
marginFactor: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
bestCase: {
|
||||
maintenanceLevel: '100',
|
||||
searchLevel: '100',
|
||||
initialLevel: '100',
|
||||
collateralReleaseLevel: '100',
|
||||
orderMarginLevel: '0',
|
||||
marginFactor: '0',
|
||||
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
},
|
||||
},
|
||||
collateralIncreaseEstimate: {
|
||||
bestCase: '0',
|
||||
worstCase: '0',
|
||||
},
|
||||
liquidation: {
|
||||
worstCase: {
|
||||
open_volume_only: worstCaseOpenVolume,
|
||||
|
||||
@@ -1,47 +1,35 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEstimatePositionQuery } from './__generated__/Positions';
|
||||
import {
|
||||
type EstimatePositionQueryVariables,
|
||||
useEstimatePositionQuery,
|
||||
} from './__generated__/Positions';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { useT } from '../use-t';
|
||||
import { MarginMode } from '@vegaprotocol/types';
|
||||
|
||||
export const LiquidationPrice = ({
|
||||
marketId,
|
||||
openVolume,
|
||||
averageEntryPrice,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
marginMode = MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
marginFactor,
|
||||
decimalPlaces,
|
||||
className,
|
||||
}: {
|
||||
marketId: string;
|
||||
openVolume: string;
|
||||
averageEntryPrice: string;
|
||||
generalAccountBalance: string;
|
||||
marginAccountBalance: string;
|
||||
orderMarginAccountBalance: string;
|
||||
marginMode: MarginMode;
|
||||
marginFactor: string;
|
||||
...variables
|
||||
}: Pick<
|
||||
EstimatePositionQueryVariables,
|
||||
| 'marketId'
|
||||
| 'openVolume'
|
||||
| 'orderMarginAccountBalance'
|
||||
| 'generalAccountBalance'
|
||||
| 'averageEntryPrice'
|
||||
| 'marginAccountBalance'
|
||||
| 'marginMode'
|
||||
| 'marginFactor'
|
||||
> & {
|
||||
decimalPlaces: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const { data: currentData, previousData } = useEstimatePositionQuery({
|
||||
variables: {
|
||||
marketId,
|
||||
openVolume,
|
||||
averageEntryPrice,
|
||||
generalAccountBalance,
|
||||
marginAccountBalance,
|
||||
orderMarginAccountBalance,
|
||||
marginMode,
|
||||
marginFactor,
|
||||
},
|
||||
variables,
|
||||
fetchPolicy: 'no-cache',
|
||||
skip: !openVolume || openVolume === '0',
|
||||
skip: !variables.openVolume || variables.openVolume === '0',
|
||||
});
|
||||
|
||||
const data = currentData || previousData;
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface Position {
|
||||
quantum: string;
|
||||
lossSocializationAmount: string;
|
||||
marginAccountBalance: string;
|
||||
orderAccountBalance: string;
|
||||
orderMarginAccountBalance: string;
|
||||
generalAccountBalance: string;
|
||||
marketDecimalPlaces: number;
|
||||
marketId: string;
|
||||
@@ -67,6 +67,7 @@ export interface Position {
|
||||
realisedPNL: string;
|
||||
status: PositionStatus;
|
||||
totalBalance: string;
|
||||
totalMarginAccountBalance: string;
|
||||
unrealisedPNL: string;
|
||||
updatedAt: string | null;
|
||||
productType: ProductType;
|
||||
@@ -119,7 +120,7 @@ export const getMetrics = (
|
||||
marginAccount?.balance ?? 0,
|
||||
asset.decimals
|
||||
);
|
||||
const orderAccountBalance = toBigNum(
|
||||
const orderMarginAccountBalance = toBigNum(
|
||||
orderAccount?.balance ?? 0,
|
||||
asset.decimals
|
||||
);
|
||||
@@ -137,12 +138,14 @@ export const getMetrics = (
|
||||
: openVolume.multipliedBy(-1)
|
||||
).multipliedBy(markPrice)
|
||||
: undefined;
|
||||
const totalBalance = marginAccountBalance
|
||||
.plus(generalAccountBalance)
|
||||
.plus(orderAccountBalance);
|
||||
const totalMarginAccountBalance = marginAccountBalance.plus(
|
||||
orderMarginAccountBalance
|
||||
);
|
||||
const totalBalance = totalMarginAccountBalance.plus(generalAccountBalance);
|
||||
|
||||
const marginMode =
|
||||
margin?.marginMode || MarginMode.MARGIN_MODE_CROSS_MARGIN;
|
||||
const marginFactor = margin?.marginFactor;
|
||||
const marginFactor = margin?.marginFactor || '1';
|
||||
const currentLeverage =
|
||||
marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN
|
||||
? (marginFactor && 1 / Number(marginFactor)) || undefined
|
||||
@@ -153,7 +156,7 @@ export const getMetrics = (
|
||||
: undefined;
|
||||
metrics.push({
|
||||
marginMode,
|
||||
marginFactor: marginFactor || '0',
|
||||
marginFactor,
|
||||
maintenanceLevel: margin?.maintenanceLevel,
|
||||
assetId: asset.id,
|
||||
assetSymbol: asset.symbol,
|
||||
@@ -163,7 +166,7 @@ export const getMetrics = (
|
||||
quantum: asset.quantum,
|
||||
lossSocializationAmount: position.lossSocializationAmount || '0',
|
||||
marginAccountBalance: marginAccount?.balance ?? '0',
|
||||
orderAccountBalance: orderAccount?.balance ?? '0',
|
||||
orderMarginAccountBalance: orderAccount?.balance ?? '0',
|
||||
generalAccountBalance: generalAccount?.balance ?? '0',
|
||||
marketDecimalPlaces,
|
||||
marketId: market.id,
|
||||
@@ -180,6 +183,9 @@ export const getMetrics = (
|
||||
realisedPNL: position.realisedPNL,
|
||||
status: position.positionStatus,
|
||||
totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(),
|
||||
totalMarginAccountBalance: totalMarginAccountBalance
|
||||
.multipliedBy(10 ** asset.decimals)
|
||||
.toFixed(),
|
||||
unrealisedPNL: position.unrealisedPNL,
|
||||
updatedAt: position.updatedAt || null,
|
||||
productType: market?.tradableInstrument.instrument.product
|
||||
@@ -269,13 +275,26 @@ const positionDataProvider = makeDerivedDataProvider<
|
||||
}
|
||||
);
|
||||
|
||||
export type OpenVolumeData = Pick<
|
||||
PositionFieldsFragment,
|
||||
'openVolume' | 'averageEntryPrice'
|
||||
>;
|
||||
|
||||
export const openVolumeDataProvider = makeDerivedDataProvider<
|
||||
string,
|
||||
OpenVolumeData,
|
||||
never,
|
||||
PositionsQueryVariables & MarketDataQueryVariables
|
||||
>(
|
||||
[positionDataProvider],
|
||||
(data) => (data[0] as PositionFieldsFragment | null)?.openVolume || null
|
||||
>([positionDataProvider], ([data], variables, previousData) =>
|
||||
produce(previousData, (draft) => {
|
||||
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 = (
|
||||
@@ -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<
|
||||
number,
|
||||
never,
|
||||
@@ -392,15 +436,7 @@ export const maxLeverageProvider = makeDerivedDataProvider<
|
||||
const market: MarketInfo | null = parts[0];
|
||||
const position: PositionFieldsFragment | null = parts[1];
|
||||
const margin: MarginFieldsFragment | null = parts[2];
|
||||
if (!market || !market?.riskFactors) {
|
||||
return 1;
|
||||
}
|
||||
const maxLeverage =
|
||||
1 /
|
||||
(Math.max(
|
||||
Number(market.riskFactors.long),
|
||||
Number(market.riskFactors.short)
|
||||
) || 1);
|
||||
const maxLeverage = getMaxLeverage(market);
|
||||
|
||||
if (
|
||||
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({
|
||||
dataProvider: maxLeverageProvider,
|
||||
dataProvider: partyId ? maxLeverageProvider : maxMarketLeverageProvider,
|
||||
variables: { marketId, partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -137,58 +137,61 @@ const PositionMargin = ({ data }: { data: Position }) => {
|
||||
? (
|
||||
BigInt(data.marginAccountBalance) + BigInt(data.generalAccountBalance)
|
||||
).toString()
|
||||
: BigInt(data.marginAccountBalance) > BigInt(data.orderAccountBalance)
|
||||
: BigInt(data.marginAccountBalance) >
|
||||
BigInt(data.orderMarginAccountBalance)
|
||||
? data.marginAccountBalance
|
||||
: data.orderAccountBalance;
|
||||
: data.orderMarginAccountBalance;
|
||||
const getWidth = (balance: string) =>
|
||||
BigNumber(balance).multipliedBy(100).dividedBy(max).toNumber();
|
||||
const inCrossMode = data.marginMode === MarginMode.MARGIN_MODE_CROSS_MARGIN;
|
||||
const hasOrderAccountBalance =
|
||||
!inCrossMode && data.orderAccountBalance !== '0';
|
||||
const hasOrderMarginAccountBalance =
|
||||
!inCrossMode && data.orderMarginAccountBalance !== '0';
|
||||
|
||||
return (
|
||||
<>
|
||||
<MarginChart
|
||||
width={inCrossMode ? getWidth(data.marginAccountBalance) : undefined}
|
||||
label={t('Margin: {{balance}}', {
|
||||
balance: addDecimalsFormatNumberQuantum(
|
||||
data.marginAccountBalance,
|
||||
data.assetDecimals,
|
||||
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.marginAccountBalance !== '0' && (
|
||||
<MarginChart
|
||||
width={inCrossMode ? getWidth(data.marginAccountBalance) : undefined}
|
||||
label={t('Margin: {{balance}}', {
|
||||
balance: addDecimalsFormatNumberQuantum(
|
||||
data.marginAccountBalance,
|
||||
data.assetDecimals,
|
||||
data.quantum
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
{hasOrderAccountBalance ? (
|
||||
})}
|
||||
other={
|
||||
inCrossMode
|
||||
? 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
|
||||
width={getWidth(data.orderAccountBalance)}
|
||||
width={getWidth(data.orderMarginAccountBalance)}
|
||||
label={t('Order: {{balance}}', {
|
||||
balance: addDecimalsFormatNumber(
|
||||
data.orderAccountBalance,
|
||||
data.orderMarginAccountBalance,
|
||||
data.assetDecimals
|
||||
),
|
||||
})}
|
||||
@@ -340,20 +343,16 @@ export const PositionsTable = ({
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data.marginAccountBalance,
|
||||
data.totalMarginAccountBalance,
|
||||
data.assetDecimals
|
||||
).toNumber();
|
||||
},
|
||||
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
|
||||
if (
|
||||
!data ||
|
||||
!data.marginAccountBalance ||
|
||||
!data.marketDecimalPlaces
|
||||
) {
|
||||
if (!data || !data.totalMarginAccountBalance) {
|
||||
return null;
|
||||
}
|
||||
const margin = addDecimalsFormatNumberQuantum(
|
||||
data.marginAccountBalance,
|
||||
data.totalMarginAccountBalance,
|
||||
data.assetDecimals,
|
||||
data.quantum
|
||||
);
|
||||
@@ -364,7 +363,7 @@ export const PositionsTable = ({
|
||||
<Tooltip
|
||||
description={
|
||||
data &&
|
||||
data.marginAccountBalance !== '0' && (
|
||||
data.totalMarginAccountBalance !== '0' && (
|
||||
<PositionMargin data={data} />
|
||||
)
|
||||
}
|
||||
@@ -410,10 +409,10 @@ export const PositionsTable = ({
|
||||
className="block text-right grow"
|
||||
marketId={data.marketId}
|
||||
openVolume={data.openVolume}
|
||||
averageEntryPrice={data.averageEntryPrice}
|
||||
generalAccountBalance={data.generalAccountBalance}
|
||||
marginAccountBalance={data.marginAccountBalance}
|
||||
orderMarginAccountBalance={data.orderAccountBalance}
|
||||
averageEntryPrice={data.averageEntryPrice}
|
||||
orderMarginAccountBalance={data.orderMarginAccountBalance}
|
||||
marginFactor={data.marginFactor}
|
||||
marginMode={data.marginMode}
|
||||
decimalPlaces={data.marketDecimalPlaces}
|
||||
|
||||
@@ -181,11 +181,11 @@ const marginsFields: MarginFieldsFragment[] = [
|
||||
];
|
||||
|
||||
export const singleRow: Position = {
|
||||
marginFactor: '1',
|
||||
generalAccountBalance: '12345600',
|
||||
maintenanceLevel: '12300000',
|
||||
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
marginFactor: '1',
|
||||
orderAccountBalance: '0',
|
||||
orderMarginAccountBalance: '0',
|
||||
partyId: 'partyId',
|
||||
assetId: 'asset-id',
|
||||
assetSymbol: 'BTC',
|
||||
@@ -195,6 +195,7 @@ export const singleRow: Position = {
|
||||
quantum: '0.1',
|
||||
lossSocializationAmount: '0',
|
||||
marginAccountBalance: '12345600',
|
||||
totalMarginAccountBalance: '12345600',
|
||||
marketDecimalPlaces: 1,
|
||||
marketId: 'string',
|
||||
marketCode: 'ETHBTC.QM21',
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { openVolumeDataProvider } from './positions-data-providers';
|
||||
import {
|
||||
OpenVolumeData,
|
||||
openVolumeDataProvider,
|
||||
} from './positions-data-providers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
export const useOpenVolume = (
|
||||
partyId: string | null | undefined,
|
||||
marketId: string
|
||||
) => {
|
||||
const [openVolume, setOpenVolume] = useState<string | undefined>(undefined);
|
||||
const update = useCallback(({ data }: { data: string | null }) => {
|
||||
setOpenVolume(data ?? undefined);
|
||||
const [openVolume, setOpenVolume] = useState<OpenVolumeData | null>(null);
|
||||
const update = useCallback(({ data }: { data: OpenVolumeData | null }) => {
|
||||
setOpenVolume(data);
|
||||
return true;
|
||||
}, []);
|
||||
useDataProvider({
|
||||
|
||||
@@ -5,7 +5,9 @@ import classNames from 'classnames';
|
||||
export const LeverageSlider = (
|
||||
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 value = props.value?.[0] || props.defaultValue?.[0];
|
||||
return (
|
||||
@@ -28,6 +30,7 @@ export const LeverageSlider = (
|
||||
const higherThanValue = value && labelValue > value;
|
||||
return (
|
||||
<span
|
||||
key={labelValue}
|
||||
className="absolute flex flex-col items-center translate-x-[-50%]"
|
||||
style={{
|
||||
left: `${
|
||||
|
||||
Reference in New Issue
Block a user