From e3895795377196bb84e7d6373269db0af142fe98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Tue, 19 Mar 2024 12:49:31 +0100 Subject: [PATCH] feat(deal-ticket): add size slider (#6006) --- .../deal-ticket/deal-ticket-container.tsx | 12 +- .../deal-ticket/deal-ticket.spec.tsx | 16 ++- .../components/deal-ticket/deal-ticket.tsx | 38 ++++- libs/deal-ticket/src/hooks/use-max-size.ts | 136 ++++++++++++++++++ libs/deal-ticket/tsconfig.lib.json | 3 +- libs/deal-ticket/tsconfig.spec.json | 3 +- libs/i18n/src/locales/en/deal-ticket.json | 4 + 7 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 libs/deal-ticket/src/hooks/use-max-size.ts diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx index 8a0be46ce..71d1d2588 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx @@ -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 = ({ ) : ( = {}, marketDataOverrides: Partial = {} ) { - const joinedMarket: Market = { + const joinedMarket: MarketInfo = { ...market, ...marketOverrides, - } as Market; + } as MarketInfo; const joinedMarketData: MarketData = { ...marketData, @@ -61,6 +61,16 @@ function generateJsx( ['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} /> + field.onChange(value)} + /> {fieldState.error && ( {fieldState.error.message} diff --git a/libs/deal-ticket/src/hooks/use-max-size.ts b/libs/deal-ticket/src/hooks/use-max-size.ts new file mode 100644 index 000000000..b9f14c2bf --- /dev/null +++ b/libs/deal-ticket/src/hooks/use-max-size.ts @@ -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, + ]); diff --git a/libs/deal-ticket/tsconfig.lib.json b/libs/deal-ticket/tsconfig.lib.json index cde06e56c..6eb0b7868 100644 --- a/libs/deal-ticket/tsconfig.lib.json +++ b/libs/deal-ticket/tsconfig.lib.json @@ -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" ] } diff --git a/libs/deal-ticket/tsconfig.spec.json b/libs/deal-ticket/tsconfig.spec.json index 413a4ff79..051b9ec3d 100644 --- a/libs/deal-ticket/tsconfig.spec.json +++ b/libs/deal-ticket/tsconfig.spec.json @@ -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" ] } diff --git a/libs/i18n/src/locales/en/deal-ticket.json b/libs/i18n/src/locales/en/deal-ticket.json index 0dedb0e63..d3d599b43 100644 --- a/libs/i18n/src/locales/en/deal-ticket.json +++ b/libs/i18n/src/locales/en/deal-ticket.json @@ -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",