Files
trading_bot_v3/components/PositionCalculator.tsx
mindesbunister bd49c65867 feat: Fix position calculator visibility and add auto-detection
- Fixed position calculator not showing when analysis entry price is 0
- Added auto-detection of Long/Short position type from AI analysis recommendation
- Implemented independent price fetching from CoinGecko API
- Added current market price display with AI recommendation
- Enhanced position calculator with fallback prices for testing
- Added API endpoint /api/price for real-time price data
- Position calculator now works even when analysis lacks specific entry prices
- Shows 'Auto-detected from analysis' label when position type is determined from AI

The position calculator is now always visible and uses:
1. Current market price from CoinGecko API
2. Auto-detected position type from analysis (Long/Short)
3. Fallback prices for testing when API is unavailable
4. Default stop loss/take profit levels when not specified in analysis
2025-07-18 13:33:34 +02:00

464 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<number>(100)
const [leverage, setLeverage] = useState<number>(10)
const [positionType, setPositionType] = useState<'long' | 'short'>('long')
const [calculation, setCalculation] = useState<PositionCalculation | null>(null)
const [showAdvanced, setShowAdvanced] = useState(false)
const [marketPrice, setMarketPrice] = useState<number>(currentPrice)
// Trading parameters
const [tradingFee, setTradingFee] = useState<number>(0.1) // 0.1% fee
const [maintenanceMargin, setMaintenanceMargin] = useState<number>(0.5) // 0.5% maintenance margin
// Auto-detect position type from analysis
useEffect(() => {
if (analysis?.recommendation) {
const rec = analysis.recommendation.toLowerCase()
if (rec.includes('buy') || rec.includes('long') || rec.includes('bullish')) {
setPositionType('long')
} else if (rec.includes('sell') || rec.includes('short') || rec.includes('bearish')) {
setPositionType('short')
}
}
}, [analysis])
// Fetch current market price if not provided
useEffect(() => {
const fetchPrice = async () => {
if (currentPrice > 0) {
setMarketPrice(currentPrice)
return
}
try {
const response = await fetch(`/api/price?symbol=${symbol}`)
const data = await response.json()
if (data.price) {
setMarketPrice(data.price)
}
} catch (error) {
console.error('Failed to fetch price:', error)
// Fallback to a reasonable default for testing
setMarketPrice(symbol.includes('BTC') ? 100000 : symbol.includes('ETH') ? 4000 : 100)
}
}
fetchPrice()
}, [currentPrice, symbol])
// Calculate position metrics
const calculatePosition = () => {
const priceToUse = marketPrice || currentPrice
if (!priceToUse || priceToUse <= 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 = priceToUse
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'
? priceToUse * 0.95
: priceToUse * 1.05
takeProfit = positionType === 'long'
? priceToUse * 1.10
: priceToUse * 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 (
<div className="bg-gradient-to-br from-gray-800/50 to-gray-900/50 border border-gray-700 rounded-lg p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-bold text-white flex items-center">
<span className="w-6 h-6 bg-gradient-to-br from-green-400 to-green-600 rounded-lg flex items-center justify-center mr-3 text-sm">
📊
</span>
Position Calculator
</h3>
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-sm text-gray-400 hover:text-white transition-colors"
>
{showAdvanced ? '🔽 Hide Advanced' : '🔼 Show Advanced'}
</button>
</div>
{/* Current Price Display */}
<div className="mb-6 p-4 bg-gray-800/30 rounded-lg border border-gray-600">
<div className="flex items-center justify-between">
<div className="text-sm text-gray-400">Current Market Price</div>
<div className="text-lg font-bold text-white">
{symbol}: ${formatPrice(marketPrice || currentPrice || 0)}
</div>
</div>
{analysis?.recommendation && (
<div className="mt-2 text-sm text-cyan-400">
AI Recommendation: {analysis.recommendation}
</div>
)}
</div>
{/* Input Controls */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Investment Amount */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Investment Amount ($)
</label>
<input
type="number"
value={investmentAmount}
onChange={(e) => 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"
/>
</div>
{/* Position Type */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Position Type
{analysis?.recommendation && (
<span className="ml-2 text-xs text-cyan-400">(Auto-detected from analysis)</span>
)}
</label>
<div className="flex space-x-2">
<button
onClick={() => setPositionType('long')}
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
positionType === 'long'
? 'bg-green-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
📈 Long
</button>
<button
onClick={() => setPositionType('short')}
className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
positionType === 'short'
? 'bg-red-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
📉 Short
</button>
</div>
</div>
{/* Leverage Slider */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-300 mb-2">
Leverage: {leverage}x
</label>
<div className="relative">
<input
type="range"
min="1"
max="100"
value={leverage}
onChange={(e) => 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%)`
}}
/>
<div className="flex justify-between text-xs text-gray-400 mt-1">
<span>1x</span>
<span>25x</span>
<span>50x</span>
<span>100x</span>
</div>
</div>
</div>
</div>
{/* Advanced Settings */}
{showAdvanced && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6 p-4 bg-gray-800/30 rounded-lg border border-gray-600">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Trading Fee (%)
</label>
<input
type="number"
value={tradingFee}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Maintenance Margin (%)
</label>
<input
type="number"
value={maintenanceMargin}
onChange={(e) => 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"
/>
</div>
</div>
)}
{/* Calculation Results */}
{calculation && (
<div className="space-y-4">
{/* Position Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
<div className="text-sm text-gray-400 mb-1">Position Size</div>
<div className="text-lg font-bold text-white">
{formatCurrency(calculation.positionSize)}
</div>
</div>
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
<div className="text-sm text-gray-400 mb-1">Entry Price</div>
<div className="text-lg font-bold text-white">
${formatPrice(calculation.entryPrice)}
</div>
</div>
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
<div className="text-sm text-gray-400 mb-1">Margin Required</div>
<div className="text-lg font-bold text-white">
{formatCurrency(calculation.marginRequired)}
</div>
</div>
</div>
{/* Risk Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-red-900/20 border border-red-700 rounded-lg p-4">
<div className="text-sm text-red-400 mb-2">🚨 Risk Metrics</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-300">Stop Loss:</span>
<span className="text-red-400 font-bold">${formatPrice(calculation.stopLoss)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-300">Max Loss:</span>
<span className="text-red-400 font-bold">{formatCurrency(calculation.maxLoss)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-300">Liquidation:</span>
<span className="text-red-500 font-bold">${formatPrice(calculation.liquidationPrice)}</span>
</div>
</div>
</div>
<div className="bg-green-900/20 border border-green-700 rounded-lg p-4">
<div className="text-sm text-green-400 mb-2">💰 Profit Potential</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-300">Take Profit:</span>
<span className="text-green-400 font-bold">${formatPrice(calculation.takeProfit)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-300">Max Profit:</span>
<span className="text-green-400 font-bold">{formatCurrency(calculation.maxProfit)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-300">Risk/Reward:</span>
<span className="text-green-400 font-bold">1:{calculation.riskRewardRatio.toFixed(2)}</span>
</div>
</div>
</div>
</div>
{/* Fee Breakdown */}
<div className="bg-gray-800/30 rounded-lg p-4 border border-gray-600">
<div className="text-sm text-gray-400 mb-2">💸 Fee Breakdown</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex justify-between">
<span className="text-gray-300">Trading Fee:</span>
<span className="text-yellow-400">{formatCurrency(calculation.tradingFee)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-300">Net Investment:</span>
<span className="text-white font-bold">{formatCurrency(calculation.netInvestment)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-300">Leverage:</span>
<span className="text-blue-400 font-bold">{leverage}x</span>
</div>
</div>
</div>
{/* Risk Warning */}
{leverage > 50 && (
<div className="bg-red-900/30 border border-red-600 rounded-lg p-4">
<div className="flex items-center space-x-2">
<span className="text-red-400 text-lg"></span>
<span className="text-red-400 font-bold">High Leverage Warning</span>
</div>
<p className="text-red-300 text-sm mt-2">
Using {leverage}x leverage is extremely risky. A small price movement against your position could result in liquidation.
</p>
</div>
)}
</div>
)}
<style jsx>{`
.slider::-webkit-slider-thumb {
appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #10B981;
cursor: pointer;
border: 2px solid #065F46;
box-shadow: 0 0 0 1px #065F46;
}
.slider::-moz-range-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
background: #10B981;
cursor: pointer;
border: 2px solid #065F46;
box-shadow: 0 0 0 1px #065F46;
}
`}</style>
</div>
)
}