feat(deal-ticket): add size slider (#6006)
This commit is contained in:
parent
74f0f7bb3d
commit
e389579537
@ -6,14 +6,15 @@ import {
|
||||
import { StopOrder } from './deal-ticket-stop-order';
|
||||
import {
|
||||
useStaticMarketData,
|
||||
useMarket,
|
||||
useMarketPrice,
|
||||
marketInfoProvider,
|
||||
} from '@vegaprotocol/markets';
|
||||
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';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
interface DealTicketContainerProps {
|
||||
marketId: string;
|
||||
@ -34,7 +35,10 @@ export const DealTicketContainer = ({
|
||||
data: market,
|
||||
error: marketError,
|
||||
loading: marketLoading,
|
||||
} = useMarket(marketId);
|
||||
} = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
variables: { marketId },
|
||||
});
|
||||
|
||||
const {
|
||||
data: marketData,
|
||||
@ -70,6 +74,10 @@ export const DealTicketContainer = ({
|
||||
) : (
|
||||
<DealTicket
|
||||
{...props}
|
||||
riskFactors={market.riskFactors}
|
||||
scalingFactors={
|
||||
market.tradableInstrument.marginCalculator?.scalingFactors
|
||||
}
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
|
@ -21,7 +21,7 @@ import * as positionsTools from '@vegaprotocol/positions';
|
||||
import { OrdersDocument } from '@vegaprotocol/orders';
|
||||
import { formatForInput } from '@vegaprotocol/utils';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { Market, MarketInfo } from '@vegaprotocol/markets';
|
||||
import type { MarketData } from '@vegaprotocol/markets';
|
||||
import {
|
||||
MockedWalletProvider,
|
||||
@ -47,10 +47,10 @@ function generateJsx(
|
||||
marketOverrides: PartialDeep<Market> = {},
|
||||
marketDataOverrides: Partial<MarketData> = {}
|
||||
) {
|
||||
const joinedMarket: Market = {
|
||||
const joinedMarket: MarketInfo = {
|
||||
...market,
|
||||
...marketOverrides,
|
||||
} as Market;
|
||||
} as MarketInfo;
|
||||
|
||||
const joinedMarketData: MarketData = {
|
||||
...marketData,
|
||||
@ -61,6 +61,16 @@ function generateJsx(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<MockedWalletProvider>
|
||||
<DealTicket
|
||||
riskFactors={{
|
||||
market: market.id,
|
||||
short: '1.046438713957377',
|
||||
long: '0.526943480689886',
|
||||
}}
|
||||
scalingFactors={{
|
||||
searchLevel: 1.1,
|
||||
initialMargin: 1.5,
|
||||
collateralRelease: 1.7,
|
||||
}}
|
||||
market={joinedMarket}
|
||||
marketData={joinedMarketData}
|
||||
marketPrice={marketPrice}
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
TradingButton as Button,
|
||||
Pill,
|
||||
ExternalLink,
|
||||
Slider,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useOpenVolume } from '@vegaprotocol/positions';
|
||||
@ -55,6 +56,7 @@ import {
|
||||
} from '../../constants';
|
||||
import type {
|
||||
Market,
|
||||
MarketInfo,
|
||||
MarketData,
|
||||
StaticMarketData,
|
||||
} from '@vegaprotocol/markets';
|
||||
@ -82,11 +84,16 @@ import { useT } from '../../use-t';
|
||||
import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
|
||||
import { useMaxSize } from '../../hooks/use-max-size';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
|
||||
export interface DealTicketProps {
|
||||
scalingFactors?: NonNullable<
|
||||
MarketInfo['tradableInstrument']['marginCalculator']
|
||||
>['scalingFactors'];
|
||||
riskFactors: MarketInfo['riskFactors'];
|
||||
market: Market;
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string | null;
|
||||
@ -139,6 +146,8 @@ export const getBaseQuoteUnit = (tags?: string[] | null) =>
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
riskFactors,
|
||||
scalingFactors,
|
||||
onMarketClick,
|
||||
marketData,
|
||||
marketPrice,
|
||||
@ -179,6 +188,7 @@ export const DealTicket = ({
|
||||
|
||||
const {
|
||||
accountBalance: generalAccountBalance,
|
||||
accountDecimals,
|
||||
loading: loadingGeneralAccountBalance,
|
||||
} = useAccountBalance(asset.id);
|
||||
|
||||
@ -382,6 +392,26 @@ export const DealTicket = ({
|
||||
const disableReduceOnlyCheckbox = !nonPersistentOrder;
|
||||
const disableIcebergCheckbox = nonPersistentOrder;
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const sizeStep = determineSizeStep(market);
|
||||
|
||||
const maxSize = useMaxSize({
|
||||
accountDecimals: accountDecimals ?? undefined,
|
||||
activeOrders: activeOrders ?? undefined,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
marginAccountBalance,
|
||||
marginFactor: margin?.marginFactor,
|
||||
marginMode: margin?.marginMode,
|
||||
marketPrice: marketPrice ?? undefined,
|
||||
price,
|
||||
riskFactors,
|
||||
scalingFactors,
|
||||
side,
|
||||
sizeStep,
|
||||
type,
|
||||
generalAccountBalance,
|
||||
openVolume,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
});
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(formValues: OrderFormValues) => {
|
||||
@ -424,7 +454,6 @@ export const DealTicket = ({
|
||||
},
|
||||
});
|
||||
|
||||
const sizeStep = determineSizeStep(market);
|
||||
const quoteName = getQuoteName(market);
|
||||
const isLimitType = type === Schema.OrderType.TYPE_LIMIT;
|
||||
|
||||
@ -492,6 +521,13 @@ export const DealTicket = ({
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Slider
|
||||
min={0}
|
||||
max={maxSize.toNumber()}
|
||||
step={Number(sizeStep)}
|
||||
value={[Number(field.value)]}
|
||||
onValueChange={([value]) => field.onChange(value)}
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-size">
|
||||
{fieldState.error.message}
|
||||
|
136
libs/deal-ticket/src/hooks/use-max-size.ts
Normal file
136
libs/deal-ticket/src/hooks/use-max-size.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import type { MarketInfo } from '@vegaprotocol/markets';
|
||||
import type { OrderFieldsFragment } from '@vegaprotocol/orders';
|
||||
import { MarginMode, OrderType, Side } from '@vegaprotocol/types';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface UseMaxSizeProps {
|
||||
accountDecimals?: number;
|
||||
activeOrders?: OrderFieldsFragment[];
|
||||
decimalPlaces: number;
|
||||
generalAccountBalance: string;
|
||||
marginAccountBalance: string;
|
||||
marginFactor?: string;
|
||||
marginMode?: MarginMode;
|
||||
marketPrice?: string;
|
||||
openVolume: string;
|
||||
positionDecimalPlaces: number;
|
||||
price?: string;
|
||||
riskFactors: MarketInfo['riskFactors'];
|
||||
scalingFactors?: NonNullable<
|
||||
MarketInfo['tradableInstrument']['marginCalculator']
|
||||
>['scalingFactors'];
|
||||
side: Side;
|
||||
sizeStep: string;
|
||||
type: OrderType;
|
||||
}
|
||||
|
||||
export const useMaxSize = ({
|
||||
openVolume,
|
||||
positionDecimalPlaces,
|
||||
generalAccountBalance,
|
||||
side,
|
||||
marginMode,
|
||||
marginFactor,
|
||||
type,
|
||||
marginAccountBalance,
|
||||
accountDecimals,
|
||||
price,
|
||||
decimalPlaces,
|
||||
sizeStep,
|
||||
activeOrders,
|
||||
riskFactors,
|
||||
scalingFactors,
|
||||
marketPrice,
|
||||
}: UseMaxSizeProps) =>
|
||||
useMemo(() => {
|
||||
let maxSize = new BigNumber(0);
|
||||
const volume = toBigNum(openVolume, positionDecimalPlaces);
|
||||
const reducingPosition =
|
||||
(openVolume.startsWith('-') && side === Side.SIDE_BUY) ||
|
||||
(!openVolume.startsWith('-') && side === Side.SIDE_SELL);
|
||||
if (marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN) {
|
||||
if (!marginFactor || !price) {
|
||||
return maxSize;
|
||||
}
|
||||
const availableMargin =
|
||||
accountDecimals !== undefined
|
||||
? toBigNum(generalAccountBalance, accountDecimals).plus(
|
||||
reducingPosition && type === OrderType.TYPE_MARKET
|
||||
? toBigNum(marginAccountBalance, accountDecimals)
|
||||
: 0
|
||||
)
|
||||
: new BigNumber(0);
|
||||
maxSize = availableMargin
|
||||
.div(marginFactor)
|
||||
.div(toBigNum(price, decimalPlaces));
|
||||
} else {
|
||||
if (
|
||||
!scalingFactors?.initialMargin ||
|
||||
!riskFactors ||
|
||||
!marketPrice ||
|
||||
accountDecimals === undefined
|
||||
) {
|
||||
return maxSize;
|
||||
}
|
||||
const availableMargin = toBigNum(
|
||||
generalAccountBalance,
|
||||
accountDecimals
|
||||
).plus(toBigNum(marginAccountBalance, accountDecimals));
|
||||
// maxSize = availableMargin / scalingFactors.initialMargin / marketPrice
|
||||
maxSize = availableMargin
|
||||
.div(
|
||||
BigNumber(
|
||||
side === Side.SIDE_BUY ? riskFactors.long : riskFactors.short
|
||||
)
|
||||
)
|
||||
.div(scalingFactors.initialMargin)
|
||||
.div(toBigNum(marketPrice, decimalPlaces));
|
||||
maxSize = maxSize
|
||||
.minus(
|
||||
// subtract remaining orders
|
||||
toBigNum(
|
||||
activeOrders
|
||||
?.filter((order) => order.side === side)
|
||||
?.reduce((sum, order) => sum + BigInt(order.remaining), BigInt(0))
|
||||
.toString() || 0,
|
||||
positionDecimalPlaces
|
||||
)
|
||||
)
|
||||
.minus(
|
||||
// subtract open volume
|
||||
side === Side.SIDE_BUY
|
||||
? volume.isGreaterThan(0)
|
||||
? volume
|
||||
: 0
|
||||
: volume.isLessThan(0)
|
||||
? volume.abs()
|
||||
: 0
|
||||
);
|
||||
}
|
||||
// round to size step
|
||||
maxSize = maxSize.minus(maxSize.mod(sizeStep));
|
||||
if (reducingPosition && type === OrderType.TYPE_MARKET) {
|
||||
// add open volume if position will be reduced
|
||||
maxSize = maxSize.plus(volume.abs());
|
||||
}
|
||||
return maxSize;
|
||||
}, [
|
||||
openVolume,
|
||||
positionDecimalPlaces,
|
||||
generalAccountBalance,
|
||||
side,
|
||||
marginMode,
|
||||
marginFactor,
|
||||
type,
|
||||
marginAccountBalance,
|
||||
accountDecimals,
|
||||
price,
|
||||
decimalPlaces,
|
||||
sizeStep,
|
||||
activeOrders,
|
||||
riskFactors,
|
||||
scalingFactors,
|
||||
marketPrice,
|
||||
]);
|
@ -25,6 +25,7 @@
|
||||
"**/*.jsx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../utils/src/lib/step.ts"
|
||||
"../utils/src/lib/step.ts",
|
||||
"src/components/deal-ticket/deal-ticket.spec.tsx.disabled"
|
||||
]
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts",
|
||||
"jest.config.ts"
|
||||
"jest.config.ts",
|
||||
"src/components/deal-ticket/deal-ticket.spec.tsx.disabled"
|
||||
]
|
||||
}
|
||||
|
@ -7,10 +7,12 @@
|
||||
"{{triggerTrailingPercentOffset}}% trailing": "{{triggerTrailingPercentOffset}}% trailing",
|
||||
"A release candidate for the staging environment": "A release candidate for the staging environment",
|
||||
"above": "above",
|
||||
"Additional margin required": "Additional margin required",
|
||||
"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",
|
||||
"Available collateral": "Available collateral",
|
||||
"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.",
|
||||
@ -22,6 +24,7 @@
|
||||
"Cross": "Cross",
|
||||
"Cross margin": "Cross margin",
|
||||
"Current margin allocation": "Current margin allocation",
|
||||
"Current margin": "Current margin",
|
||||
"Custom": "Custom",
|
||||
"Deduction from collateral": "Deduction from collateral",
|
||||
"DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT": "To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.",
|
||||
@ -46,6 +49,7 @@
|
||||
"Leverage": "Leverage",
|
||||
"Limit": "Limit",
|
||||
"Liquidation": "Liquidation",
|
||||
"Liquidation estimate": "Liquidation estimate",
|
||||
"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.",
|
||||
"Liquidity fee": "Liquidity fee",
|
||||
"Long": "Long",
|
||||
|
Loading…
Reference in New Issue
Block a user