+ {partyId && (
+
+ )}
+ {partyId && (
+
+ )}
+ >
+ );
+};
diff --git a/libs/environment/src/hooks/use-environment.ts b/libs/environment/src/hooks/use-environment.ts
index de3afbc75f..893ac5f1a4 100644
--- a/libs/environment/src/hooks/use-environment.ts
+++ b/libs/environment/src/hooks/use-environment.ts
@@ -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',
diff --git a/libs/environment/src/types.ts b/libs/environment/src/types.ts
index 1e47ebe644..a94d7aa0a1 100644
--- a/libs/environment/src/types.ts
+++ b/libs/environment/src/types.ts
@@ -19,6 +19,7 @@ export type FeatureFlags = z.infer;
export type CosmicElevatorFlags = Pick<
FeatureFlags,
| 'ICEBERG_ORDERS'
+ | 'ISOLATED_MARGIN'
| 'STOP_ORDERS'
| 'SUCCESSOR_MARKETS'
| 'PRODUCT_PERPETUALS'
diff --git a/libs/environment/src/utils/validate-environment.ts b/libs/environment/src/utils/validate-environment.ts
index e2ce99700c..0fc19e90a0 100644
--- a/libs/environment/src/utils/validate-environment.ts
+++ b/libs/environment/src/utils/validate-environment.ts
@@ -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()),
diff --git a/libs/i18n/src/locales/en/deal-ticket.json b/libs/i18n/src/locales/en/deal-ticket.json
index c3bf38ff35..c90019b158 100644
--- a/libs/i18n/src/locales/en/deal-ticket.json
+++ b/libs/i18n/src/locales/en/deal-ticket.json
@@ -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."
}
diff --git a/libs/i18n/src/locales/en/positions.json b/libs/i18n/src/locales/en/positions.json
index 0d6df577a2..a37df12cd8 100644
--- a/libs/i18n/src/locales/en/positions.json
+++ b/libs/i18n/src/locales/en/positions.json
@@ -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",
diff --git a/libs/i18n/src/locales/en/web3.json b/libs/i18n/src/locales/en/web3.json
index 5aec664471..281edbeec2 100644
--- a/libs/i18n/src/locales/en/web3.json
+++ b/libs/i18n/src/locales/en/web3.json
@@ -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",
diff --git a/libs/positions/src/lib/positions-data-providers.spec.ts b/libs/positions/src/lib/positions-data-providers.spec.ts
index 3f584ddda3..d1ba1aa0c9 100644
--- a/libs/positions/src/lib/positions-data-providers.spec.ts
+++ b/libs/positions/src/lib/positions-data-providers.spec.ts
@@ -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');
diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts
index 112fb61e05..ff602a5624 100644
--- a/libs/positions/src/lib/positions-data-providers.ts
+++ b/libs/positions/src/lib/positions-data-providers.ts
@@ -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 | 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
- ? totalBalance.isEqualTo(0)
- ? new BigNumber(0)
- : notional.dividedBy(totalBalance)
- : undefined;
+ 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)
+ ? 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,
+ });
+};
diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx
index c325fc21cf..14e2f05165 100644
--- a/libs/positions/src/lib/positions-table.tsx
+++ b/libs/positions/src/lib/positions-table.tsx
@@ -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 {
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 (
+