feat(deal-ticket): add size slider (#6006)

This commit is contained in:
Bartłomiej Głownia 2024-03-19 12:49:31 +01:00 committed by GitHub
parent 74f0f7bb3d
commit e389579537
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 204 additions and 8 deletions

View File

@ -6,14 +6,15 @@ import {
import { StopOrder } from './deal-ticket-stop-order'; import { StopOrder } from './deal-ticket-stop-order';
import { import {
useStaticMarketData, useStaticMarketData,
useMarket,
useMarketPrice, useMarketPrice,
marketInfoProvider,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit'; import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
import { DealTicket } from './deal-ticket'; import { DealTicket } from './deal-ticket';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { useT } from '../../use-t'; import { useT } from '../../use-t';
import { MarginModeSelector } from './margin-mode-selector'; import { MarginModeSelector } from './margin-mode-selector';
import { useDataProvider } from '@vegaprotocol/data-provider';
interface DealTicketContainerProps { interface DealTicketContainerProps {
marketId: string; marketId: string;
@ -34,7 +35,10 @@ export const DealTicketContainer = ({
data: market, data: market,
error: marketError, error: marketError,
loading: marketLoading, loading: marketLoading,
} = useMarket(marketId); } = useDataProvider({
dataProvider: marketInfoProvider,
variables: { marketId },
});
const { const {
data: marketData, data: marketData,
@ -70,6 +74,10 @@ export const DealTicketContainer = ({
) : ( ) : (
<DealTicket <DealTicket
{...props} {...props}
riskFactors={market.riskFactors}
scalingFactors={
market.tradableInstrument.marginCalculator?.scalingFactors
}
market={market} market={market}
marketPrice={marketPrice} marketPrice={marketPrice}
marketData={marketData} marketData={marketData}

View File

@ -21,7 +21,7 @@ import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders'; import { OrdersDocument } from '@vegaprotocol/orders';
import { formatForInput } from '@vegaprotocol/utils'; import { formatForInput } from '@vegaprotocol/utils';
import type { PartialDeep } from 'type-fest'; 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 type { MarketData } from '@vegaprotocol/markets';
import { import {
MockedWalletProvider, MockedWalletProvider,
@ -47,10 +47,10 @@ function generateJsx(
marketOverrides: PartialDeep<Market> = {}, marketOverrides: PartialDeep<Market> = {},
marketDataOverrides: Partial<MarketData> = {} marketDataOverrides: Partial<MarketData> = {}
) { ) {
const joinedMarket: Market = { const joinedMarket: MarketInfo = {
...market, ...market,
...marketOverrides, ...marketOverrides,
} as Market; } as MarketInfo;
const joinedMarketData: MarketData = { const joinedMarketData: MarketData = {
...marketData, ...marketData,
@ -61,6 +61,16 @@ function generateJsx(
<MockedProvider mocks={[...mocks]}> <MockedProvider mocks={[...mocks]}>
<MockedWalletProvider> <MockedWalletProvider>
<DealTicket <DealTicket
riskFactors={{
market: market.id,
short: '1.046438713957377',
long: '0.526943480689886',
}}
scalingFactors={{
searchLevel: 1.1,
initialMargin: 1.5,
collateralRelease: 1.7,
}}
market={joinedMarket} market={joinedMarket}
marketData={joinedMarketData} marketData={joinedMarketData}
marketPrice={marketPrice} marketPrice={marketPrice}

View File

@ -25,6 +25,7 @@ import {
TradingButton as Button, TradingButton as Button,
Pill, Pill,
ExternalLink, ExternalLink,
Slider,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useOpenVolume } from '@vegaprotocol/positions'; import { useOpenVolume } from '@vegaprotocol/positions';
@ -55,6 +56,7 @@ import {
} from '../../constants'; } from '../../constants';
import type { import type {
Market, Market,
MarketInfo,
MarketData, MarketData,
StaticMarketData, StaticMarketData,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
@ -82,11 +84,16 @@ import { useT } from '../../use-t';
import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl'; import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils'; import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
import { useMaxSize } from '../../hooks/use-max-size';
export const REDUCE_ONLY_TOOLTIP = 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.'; '"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 { export interface DealTicketProps {
scalingFactors?: NonNullable<
MarketInfo['tradableInstrument']['marginCalculator']
>['scalingFactors'];
riskFactors: MarketInfo['riskFactors'];
market: Market; market: Market;
marketData: StaticMarketData; marketData: StaticMarketData;
marketPrice?: string | null; marketPrice?: string | null;
@ -139,6 +146,8 @@ export const getBaseQuoteUnit = (tags?: string[] | null) =>
export const DealTicket = ({ export const DealTicket = ({
market, market,
riskFactors,
scalingFactors,
onMarketClick, onMarketClick,
marketData, marketData,
marketPrice, marketPrice,
@ -179,6 +188,7 @@ export const DealTicket = ({
const { const {
accountBalance: generalAccountBalance, accountBalance: generalAccountBalance,
accountDecimals,
loading: loadingGeneralAccountBalance, loading: loadingGeneralAccountBalance,
} = useAccountBalance(asset.id); } = useAccountBalance(asset.id);
@ -382,6 +392,26 @@ export const DealTicket = ({
const disableReduceOnlyCheckbox = !nonPersistentOrder; const disableReduceOnlyCheckbox = !nonPersistentOrder;
const disableIcebergCheckbox = nonPersistentOrder; const disableIcebergCheckbox = nonPersistentOrder;
const featureFlags = useFeatureFlags((state) => state.flags); 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( const onSubmit = useCallback(
(formValues: OrderFormValues) => { (formValues: OrderFormValues) => {
@ -424,7 +454,6 @@ export const DealTicket = ({
}, },
}); });
const sizeStep = determineSizeStep(market);
const quoteName = getQuoteName(market); const quoteName = getQuoteName(market);
const isLimitType = type === Schema.OrderType.TYPE_LIMIT; const isLimitType = type === Schema.OrderType.TYPE_LIMIT;
@ -492,6 +521,13 @@ export const DealTicket = ({
{...field} {...field}
/> />
</FormGroup> </FormGroup>
<Slider
min={0}
max={maxSize.toNumber()}
step={Number(sizeStep)}
value={[Number(field.value)]}
onValueChange={([value]) => field.onChange(value)}
/>
{fieldState.error && ( {fieldState.error && (
<InputError testId="deal-ticket-error-message-size"> <InputError testId="deal-ticket-error-message-size">
{fieldState.error.message} {fieldState.error.message}

View 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,
]);

View File

@ -25,6 +25,7 @@
"**/*.jsx", "**/*.jsx",
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"../utils/src/lib/step.ts" "../utils/src/lib/step.ts",
"src/components/deal-ticket/deal-ticket.spec.tsx.disabled"
] ]
} }

View File

@ -16,6 +16,7 @@
"**/*.test.jsx", "**/*.test.jsx",
"**/*.spec.jsx", "**/*.spec.jsx",
"**/*.d.ts", "**/*.d.ts",
"jest.config.ts" "jest.config.ts",
"src/components/deal-ticket/deal-ticket.spec.tsx.disabled"
] ]
} }

View File

@ -7,10 +7,12 @@
"{{triggerTrailingPercentOffset}}% trailing": "{{triggerTrailingPercentOffset}}% trailing", "{{triggerTrailingPercentOffset}}% trailing": "{{triggerTrailingPercentOffset}}% trailing",
"A release candidate for the staging environment": "A release candidate for the staging environment", "A release candidate for the staging environment": "A release candidate for the staging environment",
"above": "above", "above": "above",
"Additional margin required": "Additional margin required",
"Advanced": "Advanced", "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.", "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.", "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", "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", "below": "below",
"Cancel": "Cancel", "Cancel": "Cancel",
"Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.", "Changing the margin mode 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": "Cross",
"Cross margin": "Cross margin", "Cross margin": "Cross margin",
"Current margin allocation": "Current margin allocation", "Current margin allocation": "Current margin allocation",
"Current margin": "Current margin",
"Custom": "Custom", "Custom": "Custom",
"Deduction from collateral": "Deduction from collateral", "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.", "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", "Leverage": "Leverage",
"Limit": "Limit", "Limit": "Limit",
"Liquidation": "Liquidation", "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.", "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", "Liquidity fee": "Liquidity fee",
"Long": "Long", "Long": "Long",