From 73ecc771c52b426dd7c86676db3abf597a62dacb Mon Sep 17 00:00:00 2001 From: Leon Date: Fri, 26 Sep 2025 14:16:55 +0800 Subject: [PATCH] feat: enhance PerpTradingForm with slider for size adjustment and improved price handling --- .../TradingPanel/panels/PerpTradingForm.tsx | 182 ++++++++++++++---- 1 file changed, 141 insertions(+), 41 deletions(-) diff --git a/packages/kit/src/views/Perp/components/TradingPanel/panels/PerpTradingForm.tsx b/packages/kit/src/views/Perp/components/TradingPanel/panels/PerpTradingForm.tsx index a46ad3a2e61..065f453c0ac 100644 --- a/packages/kit/src/views/Perp/components/TradingPanel/panels/PerpTradingForm.tsx +++ b/packages/kit/src/views/Perp/components/TradingPanel/panels/PerpTradingForm.tsx @@ -8,6 +8,7 @@ import { NumberSizeableText, SizableText, Skeleton, + Slider, XStack, YStack, getFontSize, @@ -81,6 +82,8 @@ function PerpTradingForm({ prevTypeRef.current = currentType; }, [formData.type, formData.price, tokenInfo?.markPx, updateForm]); + // Token Switch Effect: Handle price updates when user switches tokens + // This prevents stale price data from being used during token transitions useEffect(() => { const prevToken = prevTokenRef.current; const hasTokenChanged = @@ -93,19 +96,20 @@ function PerpTradingForm({ tokenInfo?.markPx && isDataSynced; - // Handle token switch + // Step 1: Detect token switch and mark switching state if (hasTokenChanged) { tokenSwitchingRef.current = currentTokenName; prevTokenRef.current = currentTokenName; return; // Early return to avoid price update with stale data } - // Update price after token switch when data is synchronized + // Step 2: Update price after token data is synchronized (prevents stale price) if (shouldUpdatePrice && tokenInfo.markPx) { updateForm({ price: formatPriceToSignificantDigits(tokenInfo.markPx) }); - tokenSwitchingRef.current = false; + tokenSwitchingRef.current = false; // Reset switching state } + // Step 3: Initialize token reference on first load if (!prevToken && currentTokenName) { prevTokenRef.current = currentTokenName; } @@ -115,6 +119,7 @@ function PerpTradingForm({ return activeAssetData?.leverage?.value || tokenInfo?.maxLeverage; }, [activeAssetData?.leverage?.value, tokenInfo?.maxLeverage]); + // Reference Price: Get the effective trading price (limit price or market price) const [referencePrice, referencePriceString] = useMemo(() => { let price = new BigNumber(0); if (formData.type === 'limit' && formData.price) { @@ -129,10 +134,14 @@ function PerpTradingForm({ ]; }, [formData.type, formData.price, tokenInfo?.markPx, tokenInfo?.szDecimals]); - const availableToTrade = useMemo(() => { + const { availableToTradeDisplay, availableToTradeValue } = useMemo(() => { const _availableToTrade = activeAssetData?.availableToTrade || [0, 0]; const value = _availableToTrade[formData.side === 'long' ? 0 : 1] || 0; - return new BigNumber(value).toFixed(2, BigNumber.ROUND_DOWN); + const valueBN = new BigNumber(value); + return { + availableToTradeDisplay: valueBN.toFixed(2, BigNumber.ROUND_DOWN), + availableToTradeValue: valueBN.toNumber(), + }; }, [formData.side, activeAssetData?.availableToTrade]); const [selectedSymbolPositionValue, selectedSymbolPositionSide] = @@ -147,17 +156,97 @@ function PerpTradingForm({ return [Math.abs(value), side]; }, [perpsPositions, perpsSelectedSymbol.coin]); + // Order calculations: Total value and required margin const totalValue = useMemo(() => { const size = new BigNumber(formData.size || 0); - return size.multipliedBy(referencePrice); + return size.multipliedBy(referencePrice); // Size × Price = Total Value }, [formData.size, referencePrice]); const marginRequired = useMemo(() => { return new BigNumber(formData.size || 0) .multipliedBy(referencePrice) - .dividedBy(leverage || 1); + .dividedBy(leverage || 1); // (Size × Price) ÷ Leverage = Required Margin }, [formData.size, referencePrice, leverage]); + // Slider Configuration: Calculate price, leverage, max value and current value for size slider + const sliderConfig = useMemo(() => { + // Get effective price for slider calculation (limit price or market price) + const getEffectivePrice = (): BigNumber | null => { + if (referencePrice.gt(0)) return referencePrice; + if (tokenInfo?.markPx) { + const markPx = new BigNumber(tokenInfo.markPx); + return markPx.isFinite() && markPx.gt(0) ? markPx : null; + } + return null; + }; + + // Get safe leverage value (fallback to 1x if invalid) + const getSafeLeverage = (): BigNumber => { + if (!leverage) return new BigNumber(1); + const leverageBN = new BigNumber(leverage); + return leverageBN.isFinite() && leverageBN.gt(0) + ? leverageBN + : new BigNumber(1); + }; + + const effectivePrice = getEffectivePrice(); + const safeLeverage = getSafeLeverage(); + const currentValue = new BigNumber(formData.size || 0); + + // Calculate maximum trade size: Available Balance × Leverage ÷ Price + const calculateMaxSize = (): number => { + if (!effectivePrice || effectivePrice.lte(0)) return 0; + if (!Number.isFinite(availableToTradeValue) || availableToTradeValue <= 0) + return 0; + + const maxTokens = new BigNumber(availableToTradeValue) + .multipliedBy(safeLeverage) + .dividedBy(effectivePrice) + .decimalPlaces(tokenInfo?.szDecimals ?? 2, BigNumber.ROUND_FLOOR); + + return maxTokens.isFinite() && maxTokens.gt(0) ? maxTokens.toNumber() : 0; + }; + + const maxSize = calculateMaxSize(); + const currentValueNum = currentValue.isFinite() + ? currentValue.toNumber() + : 0; + + return { + price: effectivePrice, + leverage: safeLeverage, + maxSize, + currentValue: currentValueNum, + controlledValue: maxSize > 0 ? Math.min(currentValueNum, maxSize) : 0, + isValid: !!effectivePrice && effectivePrice.gt(0) && maxSize > 0, + }; + }, [ + referencePrice, + tokenInfo?.markPx, + tokenInfo?.szDecimals, + leverage, + availableToTradeValue, + formData.size, + ]); + + const sliderStep = useMemo(() => { + const decimals = Math.min(tokenInfo?.szDecimals ?? 2, 6); + return Number((10 ** -decimals).toFixed(decimals)); + }, [tokenInfo?.szDecimals]); + + const handleSizeSliderChange = useCallback( + (value?: number) => { + if (value === undefined) return; + if (!sliderConfig.isValid) return; + const decimals = tokenInfo?.szDecimals ?? 2; + const formatted = new BigNumber(value) + .decimalPlaces(decimals, BigNumber.ROUND_FLOOR) + .toFixed(); + updateForm({ size: formatted }); + }, + [sliderConfig.isValid, tokenInfo?.szDecimals, updateForm], + ); + const handleTpslCheckboxChange = useCallback( (checked: ICheckedState) => { updateForm({ hasTpsl: !!checked }); @@ -197,7 +286,7 @@ function PerpTradingForm({ - ${availableToTrade} + ${availableToTradeDisplay} @@ -218,38 +307,26 @@ function PerpTradingForm({ /> - {formData.type === 'limit' ? ( - { - if (tokenInfo?.markPx) { - updateForm({ - price: formatPriceToSignificantDigits(tokenInfo.markPx), - }); - } - }} - value={formData.price} - onChange={(value) => updateForm({ price: value })} - szDecimals={universe?.szDecimals ?? 2} - isMobile={isMobile} - /> - ) : ( - { - if (tokenInfo?.markPx) { - updateForm({ - price: formatPriceToSignificantDigits(tokenInfo.markPx), - }); - } - }} - value={intl.formatMessage({ - id: ETranslations.perp_market_price, - })} - onChange={(value) => updateForm({ price: value })} - szDecimals={universe?.szDecimals ?? 2} - isMobile={isMobile} - disabled - /> - )} + { + if (tokenInfo?.markPx) { + updateForm({ + price: formatPriceToSignificantDigits(tokenInfo.markPx), + }); + } + }} + value={ + formData.type === 'limit' + ? formData.price + : intl.formatMessage({ + id: ETranslations.perp_market_price, + }) + } + onChange={(value) => updateForm({ price: value })} + szDecimals={universe?.szDecimals ?? 2} + isMobile={isMobile} + disabled={formData.type === 'market'} + /> updateForm({ size: value })} isMobile={isMobile} /> + {activeAssetData ? ( - ${availableToTrade} + ${availableToTradeDisplay} ) : ( @@ -433,6 +521,18 @@ function PerpTradingForm({ value={formData.size} onChange={(value) => updateForm({ size: value })} /> +