feat(trading): margin mode selector (#5575)

Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
Bartłomiej Głownia 2024-01-23 17:19:49 +01:00 committed by GitHub
parent e309669736
commit fde77ebccb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 874 additions and 135 deletions

View File

@ -21,6 +21,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=true
NX_ICEBERG_ORDERS=true
NX_METAMASK_SNAPS=true
NX_REFERRALS=true

View File

@ -21,6 +21,7 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
NX_ISOLATED_MARGIN=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false

View File

@ -20,6 +20,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true

View File

@ -21,6 +21,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=false
NX_ICEBERG_ORDERS=true
NX_METAMASK_SNAPS=true
NX_REFERRALS=true

View File

@ -21,6 +21,7 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=false
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false

View File

@ -21,6 +21,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true

View File

@ -22,6 +22,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=true
NX_ICEBERG_ORDERS=true
NX_METAMASK_SNAPS=true
NX_REFERRALS=true

View File

@ -22,6 +22,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false

View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "certifi"
@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
reference = "HEAD"
resolved_reference = "2aed8c94b25d8fa2e376d3b63ca1f9193d28cdfd"
resolved_reference = "4440abbb6ce0d3e80beba5cd01f20cd21983cbf8"
[[package]]
name = "websocket-client"

View File

@ -0,0 +1,53 @@
import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from actions.utils import next_epoch, wait_for_toast_confirmation
tooltip_content = "tooltip-content"
leverage_input = "#leverage-input"
tab_positions = "tab-positions"
margin_row = '[col-id="margin"]'
def create_position(vega: VegaServiceNull, market_id):
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 100, 110)
vega.wait_fn(1)
vega.wait_for_total_catchup
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_switch_cross_isolated_margin(
continuous_market, vega: VegaServiceNull, page: Page):
create_position(vega, continuous_market)
page.goto(f"/#/markets/{continuous_market}")
expect(page.locator(margin_row).nth(1)).to_have_text("874.21992Cross1.0x")
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
page.get_by_test_id(tab_positions).get_by_text("Cross").hover()
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
"Liquidation: 582.81328Margin: 874.21992General account: 998,084.95183"
)
page.get_by_role("button", name="Isolated 10x").click()
page.locator(leverage_input).clear()
page.locator(leverage_input).type("1")
page.get_by_role("button", name="Confirm").click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
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")
# 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()
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
"Liquidation: 583.62409Margin: 11,109.99996Order: 11,000.00"
)
page.get_by_role("button", name="Cross").click()
page.get_by_role("button", name="Confirm").click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.locator(margin_row).nth(1)).to_have_text(
"22,109.99996Cross1.0x")

View File

@ -29,13 +29,15 @@ def verify_data_grid(page: Page, data_test_id, expected_pattern):
logger.info(f"Matched: {expected} == {actual}")
else:
logger.info(f"Not Matched: {expected} != {actual}")
raise AssertionError(f"Pattern does not match: {expected} != {actual}")
raise AssertionError(
f"Pattern does not match: {expected} != {actual}")
else: # it's not a regex, so we escape it
if re.search(re.escape(expected), actual):
logger.info(f"Matched: {expected} == {actual}")
else:
logger.info(f"Not Matched: {expected} != {actual}")
raise AssertionError(f"Pattern does not match: {expected} != {actual}")
raise AssertionError(
f"Pattern does not match: {expected} != {actual}")
def submit_order(vega: VegaServiceNull, wallet_name, market_id, side, volume, price):
@ -91,7 +93,7 @@ def test_limit_order_trade_open_position(continuous_market, page: Page):
"average_entry_price": "107.50",
"mark_price": "107.50",
"margin": "8.50269",
"leverage": "1.0x",
"leverage": "Cross1.0x",
"liquidation": "0.00",
"realised_pnl": "0.00",
"unrealised_pnl": "0.00",
@ -104,7 +106,8 @@ def test_limit_order_trade_open_position(continuous_market, page: Page):
# 7004-POSI-002
size_and_notional = table.locator("[col-id='openVolume']")
expect(size_and_notional.get_by_test_id(primary_id)).to_have_text(position["size"])
expect(size_and_notional.get_by_test_id(
primary_id)).to_have_text(position["size"])
expect(size_and_notional.get_by_test_id(secondary_id)).to_have_text(
position["notional"]
)

View File

@ -28,8 +28,9 @@ def test_usage_breakdown(continuous_market, page: Page):
usage_breakdown = page.get_by_test_id("usage-breakdown")
# Verify headers
headers = ["Market", "Account type", "Balance", "Margin health"]
ag_headers = usage_breakdown.locator(".ag-header-cell-text").element_handles()
headers = ["Market", "Account type", "Balance"]
ag_headers = usage_breakdown.locator(
".ag-header-cell-text").element_handles()
for i, header_element in enumerate(ag_headers):
header_text = header_element.text_content()
assert header_text == headers[i]
@ -38,30 +39,10 @@ def test_usage_breakdown(continuous_market, page: Page):
expect(usage_breakdown.locator('[class="mb-2 text-sm"]')).to_have_text(
"You have 1,000,000.00 tDAI in total."
)
expect(usage_breakdown.locator(COL_ID_USED).first).to_have_text("8.50269 (0%)")
expect(usage_breakdown.locator(
COL_ID_USED).first).to_have_text("8.50269 (0%)")
expect(usage_breakdown.locator(COL_ID_USED).nth(1)).to_have_text(
"999,991.49731 (99%)"
)
# Maintenance Level
expect(
usage_breakdown.locator(
".ag-center-cols-container [col-id='market.id'] .ag-cell-value"
).first
).to_have_text("2.85556 above maintenance level")
# Margin health tooltip
usage_breakdown.get_by_test_id("margin-health-chart-track").hover()
tooltip_data = [
("maintenance level", "5.64713"),
("search level", "6.21184"),
("initial level", "8.47069"),
("balance", "8.50269"),
("release level", "9.60012"),
]
for index, (label, value) in enumerate(tooltip_data):
expect(page.get_by_test_id(TOOLTIP_LABEL).nth(index)).to_have_text(label)
expect(page.get_by_test_id(TOOLTIP_VALUE).nth(index)).to_have_text(value)
page.get_by_test_id("dialog-close").click()

View File

@ -3,6 +3,9 @@ fragment MarginFields on MarginLevels {
searchLevel
initialLevel
collateralReleaseLevel
marginFactor
marginMode
orderMarginLevel
asset {
id
}
@ -33,6 +36,9 @@ subscription MarginsSubscription($partyId: ID!) {
searchLevel
initialLevel
collateralReleaseLevel
marginFactor
marginMode
orderMarginLevel
timestamp
}
}

View File

@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginFactor: string, marginMode: Types.MarginMode, orderMarginLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
export type MarginsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginFactor: string, marginMode: Types.MarginMode, orderMarginLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } };
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginFactor: string, marginMode: Types.MarginMode, orderMarginLevel: string, timestamp: any } };
export const MarginFieldsFragmentDoc = gql`
fragment MarginFields on MarginLevels {
@ -25,6 +25,9 @@ export const MarginFieldsFragmentDoc = gql`
searchLevel
initialLevel
collateralReleaseLevel
marginFactor
marginMode
orderMarginLevel
asset {
id
}
@ -85,6 +88,9 @@ export const MarginsSubscriptionDocument = gql`
searchLevel
initialLevel
collateralReleaseLevel
marginFactor
marginMode
orderMarginLevel
timestamp
}
}

View File

@ -105,6 +105,7 @@ export interface AccountFields extends Account {
// The total balance of these accounts will be used for the 'used' column in the
// collateral table
const USE_ACCOUNT_TYPES = [
AccountType.ACCOUNT_TYPE_ORDER_MARGIN,
AccountType.ACCOUNT_TYPE_MARGIN,
AccountType.ACCOUNT_TYPE_BOND,
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,

View File

@ -4,14 +4,6 @@ import * as Types from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import { getAccountData } from './accounts-data-provider';
const marginHealthChartTestId = 'margin-health-chart';
jest.mock('./margin-health-chart', () => ({
MarginHealthChart: () => {
return <div data-testid={marginHealthChartTestId}></div>;
},
}));
const singleRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
@ -49,10 +41,10 @@ describe('BreakdownTable', () => {
render(<BreakdownTable data={singleRowData} />);
});
const headers = await screen.findAllByRole('columnheader');
expect(headers).toHaveLength(4);
expect(headers).toHaveLength(3);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(['Market', 'Account type', 'Balance', 'Margin health']);
).toEqual(['Market', 'Account type', 'Balance']);
});
it('should apply correct formatting', async () => {
@ -70,24 +62,6 @@ describe('BreakdownTable', () => {
cells.slice(0, -1).forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
expect(screen.getByTestId(marginHealthChartTestId)).toBeInTheDocument();
});
it('displays margin health chart only for margin account', async () => {
await act(async () => {
render(
<BreakdownTable
data={[
{
...singleRow,
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
market: null,
},
]}
/>
);
});
expect(screen.queryByTestId(marginHealthChartTestId)).toBeNull();
});
it('should get correct account data', () => {

View File

@ -16,14 +16,13 @@ import { ProgressBarCell } from '@vegaprotocol/datagrid';
import { AgGrid, PriceCell } from '@vegaprotocol/datagrid';
import type { ColDef } from 'ag-grid-community';
import { accountValuesComparator } from './accounts-table';
import { MarginHealthChart } from './margin-health-chart';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { AccountType } from '@vegaprotocol/types';
const defaultColDef = {
resizable: true,
sortable: true,
minWidth: 100,
flex: 1,
};
interface BreakdownTableProps extends AgGridReactProps {
@ -111,23 +110,6 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
},
comparator: accountValuesComparator,
},
{
headerName: t('Margin health'),
field: 'market.id',
maxWidth: 500,
sortable: false,
cellRenderer: ({
data,
}: VegaICellRendererParams<AccountFields, 'market.id'>) =>
data?.market?.id &&
data.type === AccountType['ACCOUNT_TYPE_MARGIN'] &&
data?.asset.id ? (
<MarginHealthChart
marketId={data.market.id}
assetId={data.asset.id}
/>
) : null,
},
];
return defs;
}, [t]);

View File

@ -20,14 +20,20 @@ const update = (
return produce(data || [], (draft) => {
const { marketId } = delta;
const index = draft.findIndex((node) => node.market.id === marketId);
if (index !== -1) {
const currNode = draft[index];
draft[index] = {
...currNode,
const deltaData = {
maintenanceLevel: delta.maintenanceLevel,
searchLevel: delta.searchLevel,
initialLevel: delta.initialLevel,
collateralReleaseLevel: delta.collateralReleaseLevel,
marginFactor: delta.marginFactor,
marginMode: delta.marginMode,
orderMarginLevel: delta.orderMarginLevel,
};
if (index !== -1) {
const currNode = draft[index];
draft[index] = {
...currNode,
...deltaData,
};
} else {
draft.unshift({
@ -36,10 +42,7 @@ const update = (
__typename: 'Market',
id: delta.marketId,
},
maintenanceLevel: delta.maintenanceLevel,
searchLevel: delta.searchLevel,
initialLevel: delta.initialLevel,
collateralReleaseLevel: delta.collateralReleaseLevel,
...deltaData,
asset: {
__typename: 'Asset',
id: delta.asset,

View File

@ -5,6 +5,7 @@ import {
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',
@ -18,6 +19,9 @@ const margins: MarginFieldsFragment = {
initialLevel: '800',
searchLevel: '600',
maintenanceLevel: '400',
marginFactor: '',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginLevel: '',
market: {
id: 'marketId',
},

View File

@ -13,6 +13,7 @@ import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
import { DealTicket } from './deal-ticket';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useT } from '../../use-t';
import { MarginModeSelector } from './margin-mode-selector';
interface DealTicketContainerProps {
marketId: string;
@ -51,11 +52,20 @@ export const DealTicketContainer = ({
reload={reload}
>
{market && marketData ? (
featureFlags.STOP_ORDERS && showStopOrder ? (
<>
{featureFlags.ISOLATED_MARGIN && (
<>
<MarginModeSelector marketId={marketId} />
<hr className="border-vega-clight-500 dark:border-vega-cdark-500 mb-4" />
</>
)}
{featureFlags.STOP_ORDERS && showStopOrder ? (
<StopOrder
market={market}
marketPrice={marketPrice}
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
submit={(stopOrdersSubmission) =>
create({ stopOrdersSubmission })
}
/>
) : (
<DealTicket
@ -65,7 +75,8 @@ export const DealTicketContainer = ({
marketData={marketData}
submit={(orderSubmission) => create({ orderSubmission })}
/>
)
)}
</>
) : (
<p>{t('Could not load market')}</p>
)}

View File

@ -0,0 +1,246 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
TradingButton as Button,
TradingInput as Input,
FormGroup,
LeverageSlider,
} from '@vegaprotocol/ui-toolkit';
import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet';
import * as Types from '@vegaprotocol/types';
import {
type VegaTransactionStore,
useVegaTransactionStore,
} from '@vegaprotocol/web3';
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';
const defaultLeverage = 10;
interface MarginDialogProps {
open: boolean;
onClose: () => void;
marketId: string;
partyId: string;
create: VegaTransactionStore['create'];
}
const CrossMarginModeDialog = ({
open,
onClose,
marketId,
create,
}: MarginDialogProps) => {
const t = useT();
return (
<Dialog
title={t('Cross margin')}
size="small"
open={open}
onChange={(isOpen) => {
if (!isOpen) {
onClose();
}
}}
>
<div className="text-sm mb-4">
<p className="mb-1">
{t('You are setting this market to cross-margin mode.')}
</p>
<p className="mb-1">
{t(
'Your max leverage on each position will be determined by the risk model of the market.'
)}
</p>
<p>
{t(
'All available funds in your general account will be used to finance your margin if the market moves against you.'
)}
</p>
</div>
<Button
className="w-full"
onClick={() => {
create({
updateMarginMode: {
market_id: marketId,
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
});
onClose();
}}
>
{t('Confirm')}
</Button>
</Dialog>
);
};
const IsolatedMarginModeDialog = ({
open,
onClose,
marketId,
partyId,
marginFactor,
create,
}: MarginDialogProps & { marginFactor: string }) => {
const [leverage, setLeverage] = useState(
Number((1 / Number(marginFactor)).toFixed(1))
);
const { data: maxLeverage } = useMaxLeverage(marketId, partyId);
const max = Math.floor((maxLeverage || 1) * 10) / 10;
useEffect(() => {
setLeverage(Number((1 / Number(marginFactor)).toFixed(1)));
}, [marginFactor]);
useEffect(() => {
if (maxLeverage && leverage > max) {
setLeverage(max);
}
}, [max, maxLeverage, leverage]);
const t = useT();
return (
<Dialog
title={t('Isolated margin')}
size="small"
open={open}
onChange={(isOpen) => {
if (!isOpen) {
onClose();
}
}}
>
<div className="text-sm mb-4">
<p className="mb-1">
{t('You are setting this market to isolated margin mode.')}
</p>
<p className="mb-1">
{t(
'Set the leverage you want below. The maximum leverage you can take is determined by the risk model of the market.'
)}
</p>
<p className="mb-1">
{t(
'Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.'
)}
</p>
</div>
<form
onSubmit={() => {
create({
updateMarginMode: {
market_id: marketId,
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
marginFactor: `${1 / leverage}`,
},
});
onClose();
}}
>
<FormGroup label={t('Leverage')} labelFor="leverage-input" compact>
<div className="mb-2">
<LeverageSlider
max={max}
step={0.1}
value={[leverage]}
onValueChange={([value]) => setLeverage(value)}
/>
</div>
<Input
type="number"
id="leverage-input"
min={1}
max={max}
step={0.1}
value={leverage}
onChange={(e) => setLeverage(Number(e.target.value))}
/>
</FormGroup>
<Button className="w-full" type="submit">
{t('Confirm')}
</Button>
</form>
</Dialog>
);
};
export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
const t = useT();
const [dialog, setDialog] = useState<'cross' | 'isolated' | ''>();
const { pubKey: partyId, isReadOnly } = useVegaWallet();
const { data: margin } = useDataProvider({
dataProvider: marketMarginDataProvider,
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';
return (
<>
<div className="mb-4 grid h-8 leading-8 font-alpha text-xs grid-cols-2">
<button
disabled={disabled}
onClick={() => partyId && setDialog('cross')}
className={classnames('rounded', {
[enabledModeClassName]:
!marginMode ||
marginMode === Types.MarginMode.MARGIN_MODE_CROSS_MARGIN,
})}
>
{t('Cross')}
</button>
<button
disabled={disabled}
onClick={() => partyId && setDialog('isolated')}
className={classnames('rounded', {
[enabledModeClassName]:
marginMode === Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
})}
>
{t('Isolated {{leverage}}x', {
leverage: marginFactor
? (1 / Number(marginFactor)).toFixed(1)
: defaultLeverage,
})}
</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}`}
/>
)}
</>
);
};

View File

@ -323,6 +323,9 @@ export const compileFeatureFlags = (refresh = false): FeatureFlags => {
STOP_ORDERS: TRUTHY.includes(
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
),
ISOLATED_MARGIN: TRUTHY.includes(
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
),
SUCCESSOR_MARKETS: TRUTHY.includes(
windowOrDefault(
'NX_SUCCESSOR_MARKETS',

View File

@ -19,6 +19,7 @@ export type FeatureFlags = z.infer<typeof featureFlagsSchema>;
export type CosmicElevatorFlags = Pick<
FeatureFlags,
| 'ICEBERG_ORDERS'
| 'ISOLATED_MARGIN'
| 'STOP_ORDERS'
| 'SUCCESSOR_MARKETS'
| 'PRODUCT_PERPETUALS'

View File

@ -76,6 +76,7 @@ export const envSchema = z
const COSMIC_ELEVATOR_FLAGS = {
SUCCESSOR_MARKETS: z.optional(z.boolean()),
STOP_ORDERS: z.optional(z.boolean()),
ISOLATED_MARGIN: z.optional(z.boolean()),
ICEBERG_ORDERS: z.optional(z.boolean()),
PRODUCT_PERPETUALS: z.optional(z.boolean()),
METAMASK_SNAPS: z.optional(z.boolean()),

View File

@ -8,13 +8,17 @@
"A release candidate for the staging environment": "A release candidate for the staging environment",
"above": "above",
"Advanced": "Advanced",
"All available funds in your general account will be used to finance your margin if the market moves against you.": "All available funds in your general account will be used to finance your margin if the market moves against you.",
"An estimate of the most you would be expected to pay in fees, in the market's settlement asset {{assetSymbol}}. Fees estimated are \"taker\" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.": "An estimate of the most you would be expected to pay in fees, in the market's settlement asset {{assetSymbol}}. Fees estimated are \"taker\" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.",
"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",
"Closed": "Closed",
"Closing on {{time}}": "Closing on {{time}}",
"Confirm": "Confirm",
"Could not load market": "Could not load market",
"Cross": "Cross",
"Cross margin": "Cross margin",
"Current margin allocation": "Current margin allocation",
"Custom": "Custom",
"Deduction from collateral": "Deduction from collateral",
@ -35,6 +39,9 @@
"Iceberg": "Iceberg",
"ICEBERG_TOOLTIP": "Trade only a fraction of the order size at once. After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away. For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each. Note that the full volume of the order is not hidden and is still reflected in the order book.",
"Infrastructure fee": "Infrastructure fee",
"Isolated {{leverage}}x": "Isolated {{leverage}}x",
"Isolated margin": "Isolated margin",
"Leverage": "Leverage",
"Limit": "Limit",
"Liquidation": "Liquidation",
"LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT": "This is an approximation for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.",
@ -59,6 +66,7 @@
"OCO": "OCO",
"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.",
"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}}",
@ -75,6 +83,7 @@
"Public testnet run by the Vega team, often used for incentives": "Public testnet run by the Vega team, often used for incentives",
"Reduce only": "Reduce only",
"Referral discount": "Referral discount",
"Set the leverage you want below. The maximum leverage you can take is determined by the risk model of the market.": "Set the leverage you want below. The maximum leverage you can take is determined by the risk model of the market.",
"Short": "Short",
"Size": "Size",
"Size cannot be lower than {{sizeStep}}": "Size cannot be lower than {{sizeStep}}",
@ -128,6 +137,8 @@
"VALIDATOR_TESTNET": "VALIDATOR_TESTNET",
"Volume discount": "Volume discount",
"When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.": "When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.",
"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 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.",
@ -137,5 +148,6 @@
"You need to connect your own wallet to start trading on this market": "You need to connect your own wallet to start trading on this market",
"You need to provide a minimum visible size": "You need to provide a minimum visible size",
"You need to provide a peak size": "You need to provide a peak size",
"You need to provide a size": "You need to provide a size"
"You need to provide a size": "You need to provide a size",
"Your max leverage on each position will be determined by the risk model of the market.": "Your max leverage on each position will be determined by the risk model of the market."
}

View File

@ -1,11 +1,17 @@
{
"Best case": "Best case",
"Cross": "Cross",
"Close position": "Close position",
"Entry / Mark": "Entry / Mark",
"General account: {{balance}}": "General account: {{balance}}",
"Isolated": "Isolated",
"Lifetime loss socialisation deductions: {{losses}}": "Lifetime loss socialisation deductions: {{losses}}",
"Liquidation: {{maintenanceLevel}}": "Liquidation: {{maintenanceLevel}}",
"Maintained by network": "Maintained by network",
"Margin / Leverage": "Margin / Leverage",
"Margin: {{balance}}": "Margin: {{balance}}",
"Market": "Market",
"Order: {{balance}}": "Order: {{balance}}",
"No positions": "No positions",
"Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.": "Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.",
"Read more about loss socialisation": "Read more about loss socialisation",

View File

@ -43,6 +43,7 @@
"Go to your Ethereum wallet and connect to the network {{networkName}}": "Go to your Ethereum wallet and connect to the network {{networkName}}",
"If the network is reset or has an outage, records of your withdrawal may be lost. It is recommended that you save these details in a safe place so you can still complete your withdrawal.": "If the network is reset or has an outage, records of your withdrawal may be lost. It is recommended that you save these details in a safe place so you can still complete your withdrawal.",
"Invalid asset source: {{source}}": "Invalid asset source: {{source}}",
"Isolated margin mode, leverage: {{leverage}}x": "Isolated margin mode, leverage: {{leverage}}x",
"Loading": "Loading",
"MetaMask": "MetaMask",
"MetaMask, Brave or other injected web wallet": "MetaMask, Brave or other injected web wallet",
@ -79,6 +80,7 @@
"Transfer": "Transfer",
"Transfer complete": "Transfer complete",
"Unknown": "Unknown",
"Update margin mode": "Update margin mode",
"Vega confirmation": "Vega confirmation",
"Vega is confirming your transaction...": "Vega is confirming your transaction...",
"Verifying withdrawal approval": "Verifying withdrawal approval",

View File

@ -174,13 +174,13 @@ const marketsData = [
describe('getMetrics && rejoinPositionData', () => {
it('returns positions metrics', () => {
const positionsRejoined = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsRejoined, accounts || null);
const metrics = getMetrics(positionsRejoined, accounts || null, null);
expect(metrics.length).toEqual(2);
});
it('calculates metrics', () => {
const positionsRejoined = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsRejoined, accounts || null);
const metrics = getMetrics(positionsRejoined, accounts || null, null);
expect(metrics[0].assetSymbol).toEqual('tDAI');
expect(metrics[0].averageEntryPrice).toEqual('8993727');

View File

@ -1,19 +1,26 @@
import isEqual from 'lodash/isEqual';
import produce from 'immer';
import BigNumber from 'bignumber.js';
import sortBy from 'lodash/sortBy';
import { type Account } from '@vegaprotocol/accounts';
import {
marginsDataProvider,
type Account,
type MarginFieldsFragment,
marketMarginDataProvider,
} from '@vegaprotocol/accounts';
import { accountsDataProvider } from '@vegaprotocol/accounts';
import { toBigNum, removePaginationWrapper } from '@vegaprotocol/utils';
import {
makeDataProvider,
makeDerivedDataProvider,
useDataProvider,
} from '@vegaprotocol/data-provider';
import {
type MarketMaybeWithData,
type MarketDataQueryVariables,
allMarketsWithLiveDataProvider,
getAsset,
marketInfoProvider,
type MarketInfo,
} from '@vegaprotocol/markets';
import {
PositionsDocument,
@ -26,6 +33,7 @@ import {
} from './__generated__/Positions';
import {
AccountType,
MarginMode,
MarketState,
type MarketTradingMode,
type PositionStatus,
@ -33,6 +41,8 @@ import {
} from '@vegaprotocol/types';
export interface Position {
marginMode: MarginFieldsFragment['marginMode'];
maintenanceLevel: MarginFieldsFragment['maintenanceLevel'] | undefined;
assetId: string;
assetSymbol: string;
averageEntryPrice: string;
@ -41,6 +51,8 @@ export interface Position {
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
orderAccountBalance: string;
generalAccountBalance: string;
marketDecimalPlaces: number;
marketId: string;
marketCode: string;
@ -61,7 +73,8 @@ export interface Position {
export const getMetrics = (
data: ReturnType<typeof rejoinPositionData> | null,
accounts: Account[] | null
accounts: Account[] | null,
margins: MarginFieldsFragment[] | null
): Position[] => {
if (!data || !data?.length) {
return [];
@ -75,8 +88,20 @@ export const getMetrics = (
}
const marketData = market?.data;
const margin = margins?.find((margin) => {
return margin.market?.id === market?.id;
});
const marginAccount = accounts?.find((account) => {
return account.market?.id === market?.id;
return (
account.market?.id === market?.id &&
account.type === AccountType.ACCOUNT_TYPE_MARGIN
);
});
const orderAccount = accounts?.find((account) => {
return (
account.market?.id === market?.id &&
account.type === AccountType.ACCOUNT_TYPE_ORDER_MARGIN
);
});
const asset = getAsset(market);
const generalAccount = accounts?.find(
@ -93,6 +118,10 @@ export const getMetrics = (
marginAccount?.balance ?? 0,
asset.decimals
);
const orderAccountBalance = toBigNum(
orderAccount?.balance ?? 0,
asset.decimals
);
const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0,
asset.decimals
@ -107,21 +136,33 @@ export const getMetrics = (
: openVolume.multipliedBy(-1)
).multipliedBy(markPrice)
: undefined;
const totalBalance = marginAccountBalance.plus(generalAccountBalance);
const currentLeverage = notional
const totalBalance = marginAccountBalance
.plus(generalAccountBalance)
.plus(orderAccountBalance);
const marginMode =
margin?.marginMode || MarginMode.MARGIN_MODE_CROSS_MARGIN;
const marginFactor = margin?.marginFactor;
const currentLeverage =
marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN
? (marginFactor && 1 / Number(marginFactor)) || undefined
: notional
? totalBalance.isEqualTo(0)
? new BigNumber(0)
: notional.dividedBy(totalBalance)
? 0
: notional.dividedBy(totalBalance).toNumber()
: undefined;
metrics.push({
marginMode,
maintenanceLevel: margin?.maintenanceLevel,
assetId: asset.id,
assetSymbol: asset.symbol,
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
currentLeverage,
assetDecimals: asset.decimals,
quantum: asset.quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
orderAccountBalance: orderAccount?.balance ?? '0',
generalAccountBalance: generalAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
marketCode: market.tradableInstrument.instrument.code,
@ -291,6 +332,9 @@ export const positionsMarketsProvider = makeDerivedDataProvider<
).sort();
});
const firstOrSelf = (partyIds: string | string[]) =>
Array.isArray(partyIds) ? partyIds[0] : partyIds;
export const positionsMetricsProvider = makeDerivedDataProvider<
Position[],
Position[],
@ -301,18 +345,24 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
positionsDataProvider(callback, client, { partyIds: variables.partyIds }),
(callback, client, variables) =>
accountsDataProvider(callback, client, {
partyId: Array.isArray(variables.partyIds)
? variables.partyIds[0]
: variables.partyIds,
partyId: firstOrSelf(variables.partyIds),
}),
(callback, client, variables) =>
allMarketsWithLiveDataProvider(callback, client, {
marketIds: variables.marketIds,
}),
(callback, client, variables) =>
marginsDataProvider(callback, client, {
partyId: firstOrSelf(variables.partyIds),
}),
],
([positions, accounts, marketsData], variables) => {
([positions, accounts, marketsData, margins], variables) => {
const positionsData = rejoinPositionData(positions, marketsData);
const metrics = getMetrics(positionsData, accounts as Account[] | null);
const metrics = getMetrics(
positionsData,
accounts as Account[] | null,
margins
);
return preparePositions(metrics, variables.showClosed);
},
(data, delta, previousData) =>
@ -323,3 +373,67 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
return !(previousRow && isEqual(previousRow, row));
})
);
export const maxLeverageProvider = makeDerivedDataProvider<
number,
never,
{ partyId: string; marketId: string }
>(
[
(callback, client, { marketId }) =>
marketInfoProvider(callback, client, { marketId }),
(callback, client, { marketId, partyId }) =>
positionDataProvider(callback, client, { partyIds: partyId, marketId }),
marketMarginDataProvider,
],
(parts) => {
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);
if (
market &&
position?.openVolume &&
position?.openVolume !== '0' &&
margin
) {
const asset = getAsset(market);
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market;
const openVolume = toBigNum(
position.openVolume.replace(/^-/, ''),
positionDecimalPlaces
);
const averageEntryPrice = toBigNum(
position.averageEntryPrice,
marketDecimalPlaces
);
// https://github.com/vegaprotocol/specs/blob/nebula/protocol/0019-MCAL-margin_calculator.md#isolated-margin-mode
return Math.min(
averageEntryPrice
.multipliedBy(openVolume)
.dividedBy(toBigNum(margin.initialLevel, asset.decimals))
.toNumber(),
maxLeverage
);
}
return maxLeverage;
}
);
export const useMaxLeverage = (marketId: string, partyId?: string) => {
return useDataProvider({
dataProvider: maxLeverageProvider,
variables: { marketId, partyId: partyId || '' },
skip: !partyId,
});
};

View File

@ -21,6 +21,7 @@ import {
VegaIcon,
VegaIconNames,
Tooltip,
Lozenge,
} from '@vegaprotocol/ui-toolkit';
import {
volumePrefix,
@ -31,14 +32,17 @@ import {
} from '@vegaprotocol/utils';
import { type Position } from './positions-data-providers';
import {
MarginMode,
MarketTradingMode,
PositionStatus,
PositionStatusMapping,
} from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { DocsLinks, useFeatureFlags } from '@vegaprotocol/environment';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { LiquidationPrice } from './liquidation-price';
import { useT } from '../use-t';
import classnames from 'classnames';
import BigNumber from 'bignumber.js';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@ -74,6 +78,126 @@ const defaultColDef = {
minWidth: 110,
};
interface MarginChartProps {
width?: number;
label: string;
other?: string;
marker?: number;
markerLabel?: string;
className?: string;
}
const MarginChart = ({
width,
label,
other,
marker,
markerLabel,
className,
}: MarginChartProps) => {
return (
<div className={classnames('relative min-w-[208px]', className)}>
{markerLabel ? (
<div className="mb-1 whitespace-nowrap">{markerLabel}</div>
) : null}
<div
className={classnames('flex relative h-2', {
'dark:bg-vega-clight-800 bg-vega-cdark-800': other,
})}
>
<div
style={{ width: `${width || 100}%` }}
className="dark:bg-vega-clight-400 bg-vega-cdark-400"
></div>
{marker ? (
<div
className="absolute dark:border-t-vega-clight-400 border-t-vega-cdark-400 border-l-transparent border-r-transparent"
style={{
top: '-5px',
left: `${marker}%`,
borderWidth: '5px 5px 0px 5px',
transform: 'translateX(-5px)',
display: 'inline-block',
}}
></div>
) : null}
</div>
<div className="flex flex-wrap justify-between whitespace-nowrap">
<div className={classnames({ 'mr-1': other })}>{label}</div>
{other ? <div className="text-right">{other}</div> : null}
</div>
</div>
);
};
const PositionMargin = ({ data }: { data: Position }) => {
const t = useT();
const max =
data.marginMode === MarginMode.MARGIN_MODE_CROSS_MARGIN
? (
BigInt(data.marginAccountBalance) + BigInt(data.generalAccountBalance)
).toString()
: BigInt(data.marginAccountBalance) > BigInt(data.orderAccountBalance)
? data.marginAccountBalance
: data.orderAccountBalance;
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';
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.assetDecimals,
data.quantum
),
})
}
/>
{hasOrderAccountBalance ? (
<MarginChart
width={getWidth(data.orderAccountBalance)}
label={t('Order: {{balance}}', {
balance: addDecimalsFormatNumber(
data.orderAccountBalance,
data.assetDecimals
),
})}
/>
) : null}
</>
);
};
export const PositionsTable = ({
onClose,
onMarketClick,
@ -83,6 +207,7 @@ export const PositionsTable = ({
pubKey,
...props
}: Props) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const colDefs = useMemo<ColDef[]>(() => {
@ -132,7 +257,8 @@ export const PositionsTable = ({
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: { data: Position }) => {
sortable: false,
filterValueGetter: ({ data }: { data: Position }) => {
return data?.openVolume === undefined
? undefined
: toBigNum(data?.openVolume, data.positionDecimalPlaces).toNumber();
@ -209,7 +335,8 @@ export const PositionsTable = ({
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
sortable: false,
filterValueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
@ -233,7 +360,35 @@ export const PositionsTable = ({
const lev = data?.currentLeverage ? data.currentLeverage : 1;
const leverage = formatNumber(Math.max(1, lev), 1);
return <StackedCell primary={margin} secondary={leverage + 'x'} />;
return (
<Tooltip
description={
data &&
data.marginAccountBalance !== '0' && (
<PositionMargin data={data} />
)
}
>
<div>
<StackedCell
primary={margin}
secondary={
<>
{featureFlags.ISOLATED_MARGIN && (
<Lozenge className="mr-1">
{data?.marginMode ===
MarginMode.MARGIN_MODE_ISOLATED_MARGIN
? t('Isolated')
: t('Cross')}
</Lozenge>
)}
{leverage}x
</>
}
/>
</div>
</Tooltip>
);
},
},
{
@ -406,7 +561,16 @@ export const PositionsTable = ({
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [isReadOnly, multipleKeys, onClose, onMarketClick, pubKey, pubKeys, t]);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
pubKey,
pubKeys,
t,
featureFlags.ISOLATED_MARGIN,
]);
return (
<AgGrid

View File

@ -130,6 +130,9 @@ const marginsFields: MarginFieldsFragment[] = [
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
marginFactor: '',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginLevel: '',
market: {
__typename: 'Market',
id: 'market-0',
@ -145,6 +148,9 @@ const marginsFields: MarginFieldsFragment[] = [
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
marginFactor: '',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginLevel: '',
market: {
__typename: 'Market',
id: 'market-1',
@ -160,6 +166,9 @@ const marginsFields: MarginFieldsFragment[] = [
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
marginFactor: '',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginLevel: '',
market: {
__typename: 'Market',
id: 'market-2',
@ -172,6 +181,10 @@ const marginsFields: MarginFieldsFragment[] = [
];
export const singleRow: Position = {
generalAccountBalance: '12345600',
maintenanceLevel: '12300000',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderAccountBalance: '0',
partyId: 'partyId',
assetId: 'asset-id',
assetSymbol: 'BTC',

View File

@ -113,6 +113,8 @@ export enum AccountType {
ACCOUNT_TYPE_MARGIN = 'ACCOUNT_TYPE_MARGIN',
/** Network treasury, per-asset treasury controlled by the network */
ACCOUNT_TYPE_NETWORK_TREASURY = 'ACCOUNT_TYPE_NETWORK_TREASURY',
/** Per asset market account for party in isolated margin mode */
ACCOUNT_TYPE_ORDER_MARGIN = 'ACCOUNT_TYPE_ORDER_MARGIN',
/** Holds pending rewards to be paid to the referrer of a party out of fees paid by the taker */
ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD = 'ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD',
/** PendingTransfers - a global account for the pending transfers pool */
@ -1986,8 +1988,14 @@ export type MarginLevels = {
initialLevel: Scalars['String'];
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
maintenanceLevel: Scalars['String'];
/** Margin factor, only relevant for isolated margin mode, else 0 */
marginFactor: Scalars['String'];
/** Margin mode of the party, cross margin or isolated margin */
marginMode: MarginMode;
/** Market in which the margin is required for this party */
market: Market;
/** When in isolated margin, the required order margin level, otherwise, 0 */
orderMarginLevel: Scalars['String'];
/** The party for this margin */
party: Party;
/** If the margin is between maintenance and search, the network will initiate a collateral search, expressed as unsigned integer */
@ -2010,8 +2018,14 @@ export type MarginLevelsUpdate = {
initialLevel: Scalars['String'];
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
maintenanceLevel: Scalars['String'];
/** Margin factor, only relevant for isolated margin mode, else 0 */
marginFactor: Scalars['String'];
/** Margin mode of the party, cross margin or isolated margin */
marginMode: MarginMode;
/** Market in which the margin is required for this party */
marketId: Scalars['ID'];
/** When in isolated margin, the required order margin level, otherwise, 0 */
orderMarginLevel: Scalars['String'];
/** The party for this margin */
partyId: Scalars['ID'];
/** If the margin is between maintenance and search, the network will initiate a collateral search (unsigned integer) */
@ -2020,6 +2034,13 @@ export type MarginLevelsUpdate = {
timestamp: Scalars['Timestamp'];
};
export enum MarginMode {
/** Party is in cross margin mode */
MARGIN_MODE_CROSS_MARGIN = 'MARGIN_MODE_CROSS_MARGIN',
/** Party is in isolated margin mode */
MARGIN_MODE_ISOLATED_MARGIN = 'MARGIN_MODE_ISOLATED_MARGIN'
}
/** Represents a product & associated parameters that can be traded on Vega, has an associated OrderBook and Trade history */
export type Market = {
__typename?: 'Market';
@ -3118,6 +3139,8 @@ export enum OrderRejectionReason {
ORDER_ERROR_INVALID_TIME_IN_FORCE = 'ORDER_ERROR_INVALID_TIME_IN_FORCE',
/** Invalid type */
ORDER_ERROR_INVALID_TYPE = 'ORDER_ERROR_INVALID_TYPE',
/** Party has insufficient funds to cover for the order margin for the new or amended order */
ORDER_ERROR_ISOLATED_MARGIN_CHECK_FAILED = 'ORDER_ERROR_ISOLATED_MARGIN_CHECK_FAILED',
/** Margin check failed - not enough available margin */
ORDER_ERROR_MARGIN_CHECK_FAILED = 'ORDER_ERROR_MARGIN_CHECK_FAILED',
/** Market is closed */
@ -3138,6 +3161,8 @@ export enum OrderRejectionReason {
ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO = 'ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO',
/** Order is out of sequence */
ORDER_ERROR_OUT_OF_SEQUENCE = 'ORDER_ERROR_OUT_OF_SEQUENCE',
/** Pegged orders are not allowed for a party in isolated margin mode */
ORDER_ERROR_PEGGED_ORDERS_NOT_ALLOWED_IN_ISOLATED_MARGIN_MODE = 'ORDER_ERROR_PEGGED_ORDERS_NOT_ALLOWED_IN_ISOLATED_MARGIN_MODE',
/** A post-only order would produce an aggressive trade and thus it has been rejected */
ORDER_ERROR_POST_ONLY_ORDER_WOULD_TRADE = 'ORDER_ERROR_POST_ONLY_ORDER_WOULD_TRADE',
/** A reduce-ony order would not reduce the party's position and thus it has been rejected */
@ -3586,6 +3611,41 @@ export type PartyLockedBalance = {
untilEpoch: Scalars['Int'];
};
/** Margin mode selected for the given party and market. */
export type PartyMarginMode = {
__typename?: 'PartyMarginMode';
/** Epoch at which the update happened. */
atEpoch: Scalars['Int'];
/** Selected margin mode. */
marginMode: MarginMode;
/** Margin factor for the market. Isolated mode only. */
margin_factor?: Maybe<Scalars['String']>;
/** Unique ID of the market. */
marketId: Scalars['ID'];
/** Maximum theoretical leverage for the market. Isolated mode only. */
max_theoretical_leverage?: Maybe<Scalars['String']>;
/** Minimum theoretical margin factor for the market. Isolated mode only. */
min_theoretical_margin_factor?: Maybe<Scalars['String']>;
/** Unique ID of the party. */
partyId: Scalars['ID'];
};
/** Edge type containing the deposit and cursor information returned by a PartyMarginModeConnection */
export type PartyMarginModeEdge = {
__typename?: 'PartyMarginModeEdge';
cursor: Scalars['String'];
node: PartyMarginMode;
};
/** Connection type for retrieving cursor-based paginated party margin modes information */
export type PartyMarginModesConnection = {
__typename?: 'PartyMarginModesConnection';
/** The party margin modes */
edges?: Maybe<Array<Maybe<PartyMarginModeEdge>>>;
/** The pagination information */
pageInfo?: Maybe<PageInfo>;
};
/**
* All staking information related to a Party.
* Contains the current recognised balance by the network and
@ -4438,6 +4498,12 @@ export type Query = {
partiesConnection?: Maybe<PartyConnection>;
/** An entity that is trading on the Vega network */
party?: Maybe<Party>;
/**
* List margin modes per party per market
*
* Get a list of all margin modes, or for a specific market ID, or party ID.
*/
partyMarginModes?: Maybe<PartyMarginModesConnection>;
/** Fetch all positions */
positions?: Maybe<PositionConnection>;
/** A governance proposal located by either its ID or reference. If both are set, ID is used. */
@ -6213,6 +6279,8 @@ export enum TransferType {
TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE = 'TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE',
/** Infrastructure fee paid from general account */
TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY = 'TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY',
/** Funds moved from order margin account to margin account. */
TRANSFER_TYPE_ISOLATED_MARGIN_LOW = 'TRANSFER_TYPE_ISOLATED_MARGIN_LOW',
/** Allocates liquidity fee earnings to each liquidity provider's network controlled liquidity fee account. */
TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE = 'TRANSFER_TYPE_LIQUIDITY_FEE_ALLOCATE',
/** Liquidity fee received into general account */
@ -6239,6 +6307,10 @@ export enum TransferType {
TRANSFER_TYPE_MTM_LOSS = 'TRANSFER_TYPE_MTM_LOSS',
/** Funds added to margin account after mark to market gain */
TRANSFER_TYPE_MTM_WIN = 'TRANSFER_TYPE_MTM_WIN',
/** Funds released from order margin account to general. */
TRANSFER_TYPE_ORDER_MARGIN_HIGH = 'TRANSFER_TYPE_ORDER_MARGIN_HIGH',
/** Funds moved from general account to order margin account. */
TRANSFER_TYPE_ORDER_MARGIN_LOW = 'TRANSFER_TYPE_ORDER_MARGIN_LOW',
/** Funds deducted from margin account after a perpetuals funding loss. */
TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS = 'TRANSFER_TYPE_PERPETUALS_FUNDING_LOSS',
/** Funds added to margin account after a perpetuals funding gain. */

View File

@ -38,6 +38,7 @@ import type { ProductType, ProposalProductType } from './product';
export const AccountTypeMapping: {
[T in AccountType]: string;
} = {
ACCOUNT_TYPE_ORDER_MARGIN: 'Per asset market account',
ACCOUNT_TYPE_BOND: 'Bond account',
ACCOUNT_TYPE_EXTERNAL: 'External account',
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: 'Infrastructure fees account',
@ -211,6 +212,8 @@ export const OrderRejectionReasonMapping: {
ORDER_ERROR_INVALID_SIZE: 'Invalid size',
ORDER_ERROR_INVALID_TIME_IN_FORCE: 'Invalid time in force',
ORDER_ERROR_INVALID_TYPE: 'Invalid type',
ORDER_ERROR_ISOLATED_MARGIN_CHECK_FAILED:
'Party has insufficient funds to cover for the order margin for the new or amended order',
ORDER_ERROR_MARGIN_CHECK_FAILED: 'Margin check failed',
ORDER_ERROR_MARKET_CLOSED: 'Market closed',
ORDER_ERROR_MISSING_GENERAL_ACCOUNT: 'Missing general account',
@ -221,6 +224,8 @@ export const OrderRejectionReasonMapping: {
ORDER_ERROR_NOT_FOUND: 'Not found',
ORDER_ERROR_OFFSET_MUST_BE_GREATER_OR_EQUAL_TO_ZERO:
'Offset must be greater or equal to zero',
ORDER_ERROR_PEGGED_ORDERS_NOT_ALLOWED_IN_ISOLATED_MARGIN_MODE:
'Pegged orders are not allowed for a party in isolated margin mode',
ORDER_ERROR_OFFSET_MUST_BE_GREATER_THAN_ZERO:
'Offset must be greater than zero',
ORDER_ERROR_OUT_OF_SEQUENCE: 'Out of sequence',
@ -475,6 +480,9 @@ export const TransferTypeMapping: TransferTypeMap = {
TRANSFER_TYPE_WIN: 'Final settlement gain',
TRANSFER_TYPE_MTM_LOSS: 'Mark to market loss',
TRANSFER_TYPE_MTM_WIN: 'Mark to market gain',
TRANSFER_TYPE_ORDER_MARGIN_HIGH: 'From order margin account to general',
TRANSFER_TYPE_ORDER_MARGIN_LOW:
'From general account to order margin account',
TRANSFER_TYPE_MARGIN_LOW: 'Margin topped up',
TRANSFER_TYPE_MARGIN_HIGH: 'Margin returned',
TRANSFER_TYPE_MARGIN_CONFISCATED: 'Margin confiscated',
@ -482,6 +490,8 @@ export const TransferTypeMapping: TransferTypeMap = {
TRANSFER_TYPE_MAKER_FEE_RECEIVE: 'Maker fee received',
TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY: 'Infrastructure fee paid',
TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE: 'Infrastructure fee distributed',
TRANSFER_TYPE_ISOLATED_MARGIN_LOW:
'From order margin account to margin account',
TRANSFER_TYPE_LIQUIDITY_FEE_PAY: 'Liquidity fee paid',
TRANSFER_TYPE_LIQUIDITY_FEE_DISTRIBUTE: 'Liquidity fee received',
TRANSFER_TYPE_BOND_LOW: 'Bond account funded',
@ -515,6 +525,10 @@ export const DescriptionTransferTypeMapping: TransferTypeMap = {
TRANSFER_TYPE_WIN: `Funds added to your general account after final settlement gain`,
TRANSFER_TYPE_MTM_LOSS: `Funds deducted from your margin account after mark to market loss`,
TRANSFER_TYPE_MTM_WIN: `Funds added to your margin account after mark to market gain`,
TRANSFER_TYPE_ORDER_MARGIN_HIGH:
'Funds released from order margin account to general',
TRANSFER_TYPE_ORDER_MARGIN_LOW:
'Funds moved from general account to order margin account',
TRANSFER_TYPE_MARGIN_LOW: `Funds deducted from your general account to meet margin requirement`,
TRANSFER_TYPE_MARGIN_HIGH: `Excess margin amount returned to your general account`,
TRANSFER_TYPE_MARGIN_CONFISCATED: `Margin confiscated from your margin account to fulfil closeout`,
@ -522,6 +536,8 @@ export const DescriptionTransferTypeMapping: TransferTypeMap = {
TRANSFER_TYPE_MAKER_FEE_RECEIVE: `Maker fee received into your general account when your passive order was filled`,
TRANSFER_TYPE_INFRASTRUCTURE_FEE_PAY: `Infrastructure fee paid from your general account when your order was filled`,
TRANSFER_TYPE_INFRASTRUCTURE_FEE_DISTRIBUTE: `Infrastructure fee received: Infrastructure fee, paid by traders, received into your general account`,
TRANSFER_TYPE_ISOLATED_MARGIN_LOW:
'Funds moved from order margin account to margin account',
TRANSFER_TYPE_LIQUIDITY_FEE_PAY: `Liquidity fee paid from your general account to market's liquidity providers`,
TRANSFER_TYPE_LIQUIDITY_FEE_DISTRIBUTE: `Liquidity fee received into your general account from traders`,
TRANSFER_TYPE_BOND_LOW: `Funds deducted from your general account to meet your required liquidity bond amount`,

View File

@ -1 +1,2 @@
export * from './slider';
export * from './leverage-slider';

View File

@ -451,7 +451,22 @@ export type CreateReferralSet = {
};
};
export enum MarginMode {
MARGIN_MODE_CROSS_MARGIN = 1,
MARGIN_MODE_ISOLATED_MARGIN,
}
export interface UpdateMarginMode {
market_id: string;
mode: MarginMode;
marginFactor?: string;
}
export interface UpdateMarginModeBody {
updateMarginMode: UpdateMarginMode;
}
export type Transaction =
| UpdateMarginModeBody
| StopOrdersSubmissionBody
| StopOrdersCancellationBody
| OrderSubmissionBody
@ -468,6 +483,10 @@ export type Transaction =
| ApplyReferralCode
| CreateReferralSet;
export const isMarginModeUpdateTransaction = (
transaction: Transaction
): transaction is UpdateMarginModeBody => 'updateMarginMode' in transaction;
export const isWithdrawTransaction = (
transaction: Transaction
): transaction is WithdrawSubmissionBody => 'withdrawSubmission' in transaction;

View File

@ -10,6 +10,7 @@ import {
isStopOrdersSubmissionTransaction,
isStopOrdersCancellationTransaction,
determineId,
isMarginModeUpdateTransaction,
} from '@vegaprotocol/wallet';
import { create } from 'zustand';
@ -58,7 +59,7 @@ export interface VegaTransactionStore {
export const useVegaTransactionStore = create<VegaTransactionStore>()(
subscribeWithSelector((set, get) => ({
transactions: [] as VegaStoredTxState[],
transactions: [] as (VegaStoredTxState | undefined)[],
create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => {
const transactions = get().transactions;
const now = new Date();
@ -205,17 +206,23 @@ export const useVegaTransactionStore = create<VegaTransactionStore>()(
isStopOrdersCancellationTransaction(transaction.body);
const isConfirmedStopOrderSubmission =
isStopOrdersSubmissionTransaction(transaction.body);
const isConfirmedMarginModeTransaction =
isMarginModeUpdateTransaction(transaction.body);
if (
(isConfirmedOrderCancellation ||
isConfirmedOrderCancellation ||
isConfirmedTransfer ||
isConfirmedStopOrderCancellation ||
isConfirmedStopOrderSubmission) &&
!transactionResult.error &&
transactionResult.status
isConfirmedStopOrderSubmission ||
isConfirmedMarginModeTransaction
) {
if (transactionResult.error) {
transaction.status = VegaTxStatus.Error;
transaction.error = new Error(transactionResult.error);
} else if (transactionResult.status) {
transaction.status = VegaTxStatus.Complete;
}
}
transaction.dialogOpen = true;
transaction.updatedAt = new Date();
}

View File

@ -7,6 +7,7 @@ import type {
OrderSubmission,
StopOrdersSubmission,
StopOrderSetup,
UpdateMarginMode,
} from '@vegaprotocol/wallet';
import type {
OrderTxUpdateFieldsFragment,
@ -26,6 +27,8 @@ import {
isStopOrdersSubmissionTransaction,
isStopOrdersCancellationTransaction,
isReferralRelatedTransaction,
isMarginModeUpdateTransaction,
MarginMode,
} from '@vegaprotocol/wallet';
import { useVegaTransactionStore } from './use-vega-transaction-store';
import { VegaTxStatus } from './types';
@ -163,6 +166,7 @@ const isClosePositionTransaction = (tx: VegaStoredTxState) => {
};
const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
const marginModeUpdate = isMarginModeUpdateTransaction(tx.body);
const withdraw = isWithdrawTransaction(tx.body);
const submitOrder = isOrderSubmissionTransaction(tx.body);
const cancelOrder = isOrderCancellationTransaction(tx.body);
@ -173,6 +177,7 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
const transfer = isTransferTransaction(tx.body);
const referral = isReferralRelatedTransaction(tx.body);
return (
marginModeUpdate ||
withdraw ||
submitOrder ||
cancelOrder ||
@ -445,6 +450,27 @@ const CancelOrderDetails = ({
);
};
const MarginModeDetails = ({ data }: { data: UpdateMarginMode }) => {
const t = useT();
const { data: markets } = useMarketsMapProvider();
const marketId = data.market_id;
const market = marketId && markets?.[marketId];
if (!market) {
return null;
}
return (
<Panel>
<h4>{t('Update margin mode')}</h4>
<p>{market?.tradableInstrument.instrument.code}</p>
{data.mode === MarginMode.MARGIN_MODE_CROSS_MARGIN
? t('Cross margin mode')
: t('Isolated margin mode, leverage: {{leverage}}x', {
leverage: (1 / Number(data.marginFactor)).toFixed(1),
})}
</Panel>
);
};
const CancelStopOrderDetails = ({ stopOrderId }: { stopOrderId: string }) => {
const t = useT();
const formatTrigger = useFormatTrigger();
@ -598,6 +624,10 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
);
}
if (isMarginModeUpdateTransaction(tx.body)) {
return <MarginModeDetails data={tx.body.updateMarginMode} />;
}
if (isClosePositionTransaction(tx)) {
const transaction = tx.body as BatchMarketInstructionSubmissionBody;
const marketId = first(