- Create /api/batch-analysis endpoint that collects ALL screenshots first - Then sends all screenshots to AI for comprehensive analysis - Fixes issue where individual timeframes were analyzed immediately - Multi-timeframe analysis now provides cross-timeframe consensus - Update AIAnalysisPanel to use batch analysis for multiple timeframes - Maintains backward compatibility with single timeframe analysis
504 lines
19 KiB
TypeScript
504 lines
19 KiB
TypeScript
// 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
|
||
|
||
// Debug logging
|
||
console.log('📊 PositionCalculator mounted with:', { analysis, currentPrice, symbol })
|
||
|
||
// 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 () => {
|
||
console.log('🔍 Fetching price for:', symbol, 'currentPrice:', currentPrice)
|
||
|
||
if (currentPrice > 0) {
|
||
console.log('✅ Using provided currentPrice:', currentPrice)
|
||
setMarketPrice(currentPrice)
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/price?symbol=${symbol}`)
|
||
const data = await response.json()
|
||
console.log('📊 Price API response:', data)
|
||
|
||
if (data.price) {
|
||
console.log('✅ Setting market price to:', data.price)
|
||
setMarketPrice(data.price)
|
||
} else {
|
||
console.error('❌ No price in API response')
|
||
setMarketPrice(symbol.includes('BTC') ? 100000 : symbol.includes('ETH') ? 4000 : 100)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Failed to fetch price:', error)
|
||
// Fallback to a reasonable default for testing
|
||
const fallbackPrice = symbol.includes('BTC') ? 100000 : symbol.includes('ETH') ? 4000 : 100
|
||
console.log('🔄 Using fallback price:', fallbackPrice)
|
||
setMarketPrice(fallbackPrice)
|
||
}
|
||
}
|
||
|
||
fetchPrice()
|
||
}, [currentPrice, symbol])
|
||
|
||
// Calculate position metrics
|
||
const calculatePosition = () => {
|
||
let priceToUse = marketPrice || currentPrice
|
||
console.log('📊 Calculating position with:', { priceToUse, marketPrice, currentPrice, investmentAmount, leverage })
|
||
|
||
// Use fallback price if no price is available
|
||
if (!priceToUse || priceToUse <= 0) {
|
||
priceToUse = symbol.includes('BTC') ? 117000 : symbol.includes('ETH') ? 4000 : symbol.includes('SOL') ? 200 : 100
|
||
console.log('🔄 Using fallback price:', priceToUse)
|
||
}
|
||
|
||
const positionSize = investmentAmount * leverage
|
||
const marginRequired = investmentAmount
|
||
const fee = positionSize * (tradingFee / 100)
|
||
const netInvestment = investmentAmount + fee
|
||
|
||
console.log('📈 Position calculation:', { positionSize, marginRequired, fee, netInvestment })
|
||
|
||
// Get AI analysis targets if available
|
||
let entryPrice = priceToUse
|
||
let stopLoss = 0
|
||
let takeProfit = 0
|
||
|
||
if (analysis && analysis.analysis) {
|
||
console.log('🤖 Using AI 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])
|
||
console.log('📍 Found entry price:', entryPrice)
|
||
}
|
||
|
||
// Look for stop loss
|
||
const stopMatch = analysisText.match(/stop[:\s]*[\$]?(\d+\.?\d*)/i)
|
||
if (stopMatch) {
|
||
stopLoss = parseFloat(stopMatch[1])
|
||
console.log('🛑 Found stop loss:', stopLoss)
|
||
}
|
||
|
||
// Look for take profit
|
||
const profitMatch = analysisText.match(/(?:take profit|target)[:\s]*[\$]?(\d+\.?\d*)/i)
|
||
if (profitMatch) {
|
||
takeProfit = parseFloat(profitMatch[1])
|
||
console.log('🎯 Found take profit:', takeProfit)
|
||
}
|
||
|
||
// 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
|
||
console.log('🔄 Using default stop loss:', stopLoss)
|
||
}
|
||
|
||
if (!takeProfit) {
|
||
takeProfit = positionType === 'long'
|
||
? entryPrice * 1.10 // 10% take profit for long
|
||
: entryPrice * 0.90 // 10% take profit for short
|
||
console.log('🔄 Using default take profit:', takeProfit)
|
||
}
|
||
} else {
|
||
console.log('📊 No AI analysis, using defaults')
|
||
// 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
|
||
}
|
||
|
||
console.log('✅ Position calculation result:', result)
|
||
setCalculation(result)
|
||
onPositionChange?.(result)
|
||
return result
|
||
}
|
||
|
||
// Recalculate when parameters change
|
||
useEffect(() => {
|
||
console.log('🔄 Recalculating position...')
|
||
calculatePosition()
|
||
}, [investmentAmount, leverage, positionType, currentPrice, analysis, tradingFee, maintenanceMargin, marketPrice])
|
||
|
||
// Force initial calculation
|
||
useEffect(() => {
|
||
console.log('🚀 Position Calculator initialized, forcing calculation...')
|
||
calculatePosition()
|
||
}, [])
|
||
|
||
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 || (symbol.includes('BTC') ? 117000 : symbol.includes('ETH') ? 4000 : symbol.includes('SOL') ? 200 : 100))}
|
||
{(!marketPrice && !currentPrice) && (
|
||
<span className="text-xs text-yellow-400 ml-2">(fallback)</span>
|
||
)}
|
||
</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>
|
||
)
|
||
}
|