From ba354c609da422cabf400feab6ceb1cd71f27e65 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Fri, 18 Jul 2025 13:16:11 +0200 Subject: [PATCH] feat: implement dynamic position calculator with leverage slider - Added comprehensive PositionCalculator component with real-time PnL calculations - Implemented dynamic leverage adjustment with slider (1x to 100x) - Added investment amount input for position sizing - Integrated liquidation price calculations based on leverage and maintenance margin - Added real-time price fetching from multiple sources (CoinGecko, CoinCap, Binance) - Implemented automatic stop loss and take profit extraction from AI analysis - Added risk/reward ratio calculations and position metrics - Included trading fee calculations and net investment display - Added position type selection (Long/Short) with dynamic PnL calculation - Integrated high leverage warning system for risk management - Added advanced settings for customizable trading fees and maintenance margins - Automatically updates calculations when analysis parameters change - Supports both manual price input and real-time market data - Fully responsive design with gradient styling matching app theme --- components/AIAnalysisPanel.tsx | 42 +++ components/PositionCalculator.tsx | 407 ++++++++++++++++++++++++++++++ lib/price-fetcher.ts | 152 +++++++++++ 3 files changed, 601 insertions(+) create mode 100644 components/PositionCalculator.tsx create mode 100644 lib/price-fetcher.ts diff --git a/components/AIAnalysisPanel.tsx b/components/AIAnalysisPanel.tsx index e787c6c..aca9133 100644 --- a/components/AIAnalysisPanel.tsx +++ b/components/AIAnalysisPanel.tsx @@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react' import TradeModal from './TradeModal' import ScreenshotGallery from './ScreenshotGallery' +import PositionCalculator from './PositionCalculator' +import PriceFetcher from '../lib/price-fetcher' const layouts = (process.env.NEXT_PUBLIC_TRADINGVIEW_LAYOUTS || 'ai,Diy module').split(',').map(l => l.trim()) const timeframes = [ @@ -65,6 +67,7 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP const [enlargedScreenshot, setEnlargedScreenshot] = useState(null) const [tradeModalOpen, setTradeModalOpen] = useState(false) const [tradeModalData, setTradeModalData] = useState(null) + const [currentPrice, setCurrentPrice] = useState(0) // Helper function to safely render any value const safeRender = (value: any): string => { @@ -118,6 +121,25 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP } }, [eventSource]) + // Fetch current price when symbol changes + React.useEffect(() => { + const fetchPrice = async () => { + try { + const price = await PriceFetcher.getCurrentPrice(symbol) + setCurrentPrice(price) + } catch (error) { + console.error('Error fetching price:', error) + } + } + + fetchPrice() + + // Set up periodic price updates every 30 seconds + const interval = setInterval(fetchPrice, 30000) + + return () => clearInterval(interval) + }, [symbol]) + const toggleLayout = (layout: string) => { setSelectedLayouts(prev => prev.includes(layout) @@ -1375,6 +1397,26 @@ export default function AIAnalysisPanel({ onAnalysisComplete }: AIAnalysisPanelP )} + {/* Position Calculator */} + {result && result.analysis && ( +
+ { + console.log('Position calculation updated:', position) + }} + /> +
+ )} + {result && !result.analysis && result.screenshots && (

Screenshots Captured

diff --git a/components/PositionCalculator.tsx b/components/PositionCalculator.tsx new file mode 100644 index 0000000..eaf14fa --- /dev/null +++ b/components/PositionCalculator.tsx @@ -0,0 +1,407 @@ +// Dynamic Position Calculator Component +"use client" +import React, { useState, useEffect } from 'react' + +interface PositionCalculatorProps { + analysis?: any // The AI analysis results + currentPrice?: number + symbol?: string + onPositionChange?: (position: PositionCalculation) => void +} + +interface PositionCalculation { + investmentAmount: number + leverage: number + positionSize: number + entryPrice: number + stopLoss: number + takeProfit: number + liquidationPrice: number + maxLoss: number + maxProfit: number + riskRewardRatio: number + marginRequired: number + tradingFee: number + netInvestment: number +} + +export default function PositionCalculator({ + analysis, + currentPrice = 0, + symbol = 'BTCUSD', + onPositionChange +}: PositionCalculatorProps) { + const [investmentAmount, setInvestmentAmount] = useState(100) + const [leverage, setLeverage] = useState(10) + const [positionType, setPositionType] = useState<'long' | 'short'>('long') + const [calculation, setCalculation] = useState(null) + const [showAdvanced, setShowAdvanced] = useState(false) + + // Trading parameters + const [tradingFee, setTradingFee] = useState(0.1) // 0.1% fee + const [maintenanceMargin, setMaintenanceMargin] = useState(0.5) // 0.5% maintenance margin + + // Calculate position metrics + const calculatePosition = () => { + if (!currentPrice || currentPrice <= 0) return null + + const positionSize = investmentAmount * leverage + const marginRequired = investmentAmount + const fee = positionSize * (tradingFee / 100) + const netInvestment = investmentAmount + fee + + // Get AI analysis targets if available + let entryPrice = currentPrice + let stopLoss = 0 + let takeProfit = 0 + + if (analysis && analysis.analysis) { + // Try to extract entry, stop loss, and take profit from AI analysis + const analysisText = analysis.analysis.toLowerCase() + + // Look for entry price + const entryMatch = analysisText.match(/entry[:\s]*[\$]?(\d+\.?\d*)/i) + if (entryMatch) { + entryPrice = parseFloat(entryMatch[1]) + } + + // Look for stop loss + const stopMatch = analysisText.match(/stop[:\s]*[\$]?(\d+\.?\d*)/i) + if (stopMatch) { + stopLoss = parseFloat(stopMatch[1]) + } + + // Look for take profit + const profitMatch = analysisText.match(/(?:take profit|target)[:\s]*[\$]?(\d+\.?\d*)/i) + if (profitMatch) { + takeProfit = parseFloat(profitMatch[1]) + } + + // If no specific targets found, use percentage-based defaults + if (!stopLoss) { + stopLoss = positionType === 'long' + ? entryPrice * 0.95 // 5% stop loss for long + : entryPrice * 1.05 // 5% stop loss for short + } + + if (!takeProfit) { + takeProfit = positionType === 'long' + ? entryPrice * 1.10 // 10% take profit for long + : entryPrice * 0.90 // 10% take profit for short + } + } else { + // Default targets if no analysis + stopLoss = positionType === 'long' + ? currentPrice * 0.95 + : currentPrice * 1.05 + takeProfit = positionType === 'long' + ? currentPrice * 1.10 + : currentPrice * 0.90 + } + + // Calculate liquidation price + const liquidationPrice = positionType === 'long' + ? entryPrice * (1 - (1 / leverage) + (maintenanceMargin / 100)) + : entryPrice * (1 + (1 / leverage) - (maintenanceMargin / 100)) + + // Calculate PnL + const stopLossChange = positionType === 'long' + ? (stopLoss - entryPrice) / entryPrice + : (entryPrice - stopLoss) / entryPrice + const takeProfitChange = positionType === 'long' + ? (takeProfit - entryPrice) / entryPrice + : (entryPrice - takeProfit) / entryPrice + + const maxLoss = positionSize * Math.abs(stopLossChange) + const maxProfit = positionSize * Math.abs(takeProfitChange) + const riskRewardRatio = maxProfit / maxLoss + + const result: PositionCalculation = { + investmentAmount, + leverage, + positionSize, + entryPrice, + stopLoss, + takeProfit, + liquidationPrice, + maxLoss, + maxProfit, + riskRewardRatio, + marginRequired, + tradingFee: fee, + netInvestment + } + + setCalculation(result) + onPositionChange?.(result) + return result + } + + // Recalculate when parameters change + useEffect(() => { + calculatePosition() + }, [investmentAmount, leverage, positionType, currentPrice, analysis, tradingFee, maintenanceMargin]) + + const formatCurrency = (amount: number, decimals: number = 2) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: decimals, + maximumFractionDigits: decimals + }).format(amount) + } + + const formatPrice = (price: number) => { + return new Intl.NumberFormat('en-US', { + style: 'decimal', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(price) + } + + return ( +
+
+

+ + 📊 + + Position Calculator +

+ +
+ + {/* Input Controls */} +
+ {/* Investment Amount */} +
+ + setInvestmentAmount(Number(e.target.value))} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + min="1" + step="1" + /> +
+ + {/* Position Type */} +
+ +
+ + +
+
+ + {/* Leverage Slider */} +
+ +
+ setLeverage(Number(e.target.value))} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + style={{ + background: `linear-gradient(to right, #10B981 0%, #10B981 ${leverage}%, #374151 ${leverage}%, #374151 100%)` + }} + /> +
+ 1x + 25x + 50x + 100x +
+
+
+
+ + {/* Advanced Settings */} + {showAdvanced && ( +
+
+ + setTradingFee(Number(e.target.value))} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + min="0" + max="1" + step="0.01" + /> +
+
+ + setMaintenanceMargin(Number(e.target.value))} + className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + min="0.1" + max="5" + step="0.1" + /> +
+
+ )} + + {/* Calculation Results */} + {calculation && ( +
+ {/* Position Summary */} +
+
+
Position Size
+
+ {formatCurrency(calculation.positionSize)} +
+
+
+
Entry Price
+
+ ${formatPrice(calculation.entryPrice)} +
+
+
+
Margin Required
+
+ {formatCurrency(calculation.marginRequired)} +
+
+
+ + {/* Risk Metrics */} +
+
+
🚨 Risk Metrics
+
+
+ Stop Loss: + ${formatPrice(calculation.stopLoss)} +
+
+ Max Loss: + {formatCurrency(calculation.maxLoss)} +
+
+ Liquidation: + ${formatPrice(calculation.liquidationPrice)} +
+
+
+ +
+
💰 Profit Potential
+
+
+ Take Profit: + ${formatPrice(calculation.takeProfit)} +
+
+ Max Profit: + {formatCurrency(calculation.maxProfit)} +
+
+ Risk/Reward: + 1:{calculation.riskRewardRatio.toFixed(2)} +
+
+
+
+ + {/* Fee Breakdown */} +
+
💸 Fee Breakdown
+
+
+ Trading Fee: + {formatCurrency(calculation.tradingFee)} +
+
+ Net Investment: + {formatCurrency(calculation.netInvestment)} +
+
+ Leverage: + {leverage}x +
+
+
+ + {/* Risk Warning */} + {leverage > 50 && ( +
+
+ ⚠️ + High Leverage Warning +
+

+ Using {leverage}x leverage is extremely risky. A small price movement against your position could result in liquidation. +

+
+ )} +
+ )} + + +
+ ) +} diff --git a/lib/price-fetcher.ts b/lib/price-fetcher.ts new file mode 100644 index 0000000..6f26da5 --- /dev/null +++ b/lib/price-fetcher.ts @@ -0,0 +1,152 @@ +// Price fetcher utility for getting current market prices +export class PriceFetcher { + private static cache = new Map() + private static readonly CACHE_DURATION = 30000 // 30 seconds + + static async getCurrentPrice(symbol: string): Promise { + const cacheKey = symbol.toUpperCase() + const cached = this.cache.get(cacheKey) + + // Return cached price if recent + if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { + return cached.price + } + + try { + // Try multiple price sources + let price = await this.fetchFromCoinGecko(symbol) + + if (!price) { + price = await this.fetchFromCoinCap(symbol) + } + + if (!price) { + price = await this.fetchFromBinance(symbol) + } + + if (price) { + this.cache.set(cacheKey, { price, timestamp: Date.now() }) + return price + } + + return 0 + } catch (error) { + console.error('Error fetching price:', error) + return cached?.price || 0 + } + } + + private static async fetchFromCoinGecko(symbol: string): Promise { + try { + const coinId = this.getCoinGeckoId(symbol) + if (!coinId) return null + + const response = await fetch( + `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`, + { cache: 'no-cache' } + ) + + if (!response.ok) return null + + const data = await response.json() + return data[coinId]?.usd || null + } catch (error) { + console.error('CoinGecko fetch error:', error) + return null + } + } + + private static async fetchFromCoinCap(symbol: string): Promise { + try { + const asset = this.getCoinCapAsset(symbol) + if (!asset) return null + + const response = await fetch( + `https://api.coincap.io/v2/assets/${asset}`, + { cache: 'no-cache' } + ) + + if (!response.ok) return null + + const data = await response.json() + return parseFloat(data.data?.priceUsd) || null + } catch (error) { + console.error('CoinCap fetch error:', error) + return null + } + } + + private static async fetchFromBinance(symbol: string): Promise { + try { + const binanceSymbol = this.getBinanceSymbol(symbol) + if (!binanceSymbol) return null + + const response = await fetch( + `https://api.binance.com/api/v3/ticker/price?symbol=${binanceSymbol}`, + { cache: 'no-cache' } + ) + + if (!response.ok) return null + + const data = await response.json() + return parseFloat(data.price) || null + } catch (error) { + console.error('Binance fetch error:', error) + return null + } + } + + private static getCoinGeckoId(symbol: string): string | null { + const mapping: Record = { + 'BTCUSD': 'bitcoin', + 'ETHUSD': 'ethereum', + 'SOLUSD': 'solana', + 'SUIUSD': 'sui', + 'ADAUSD': 'cardano', + 'DOTUSD': 'polkadot', + 'AVAXUSD': 'avalanche-2', + 'LINKUSD': 'chainlink', + 'MATICUSD': 'matic-network', + 'UNIUSD': 'uniswap' + } + return mapping[symbol.toUpperCase()] || null + } + + private static getCoinCapAsset(symbol: string): string | null { + const mapping: Record = { + 'BTCUSD': 'bitcoin', + 'ETHUSD': 'ethereum', + 'SOLUSD': 'solana', + 'SUIUSD': 'sui', + 'ADAUSD': 'cardano', + 'DOTUSD': 'polkadot', + 'AVAXUSD': 'avalanche', + 'LINKUSD': 'chainlink', + 'MATICUSD': 'polygon', + 'UNIUSD': 'uniswap' + } + return mapping[symbol.toUpperCase()] || null + } + + private static getBinanceSymbol(symbol: string): string | null { + const mapping: Record = { + 'BTCUSD': 'BTCUSDT', + 'ETHUSD': 'ETHUSDT', + 'SOLUSD': 'SOLUSDT', + 'SUIUSD': 'SUIUSDT', + 'ADAUSD': 'ADAUSDT', + 'DOTUSD': 'DOTUSDT', + 'AVAXUSD': 'AVAXUSDT', + 'LINKUSD': 'LINKUSDT', + 'MATICUSD': 'MATICUSDT', + 'UNIUSD': 'UNIUSDT' + } + return mapping[symbol.toUpperCase()] || null + } + + static clearCache(): void { + this.cache.clear() + } +} + +export default PriceFetcher