- Replace mock data with real market analysis in paper trading - Safe paper trading API now uses live TradingView screenshots and OpenAI analysis - Maintain complete isolation from live trading while using real market conditions - Fix Docker build error in automation trade route (removed unreachable code) - Add safety redirects to prevent accidental live trading access - Real data includes: live charts, technical indicators, current market conditions - Analysis time: 30-180s for genuine market analysis vs 5s for mock data - All safety blocks maintained for zero trading risk learning environment Tested and verified: Container builds and runs successfully Real screenshot capture working (TradingView integration) OpenAI analysis processing real market data Safety systems prevent any actual trading Paper trading provides realistic learning experience
429 lines
17 KiB
JavaScript
429 lines
17 KiB
JavaScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
|
||
export default function SafePaperTradingPage() {
|
||
const [symbol, setSymbol] = useState('SOLUSD')
|
||
const [timeframe, setTimeframe] = useState('60')
|
||
const [loading, setLoading] = useState(false)
|
||
const [currentAnalysis, setCurrentAnalysis] = useState(null)
|
||
const [paperBalance, setPaperBalance] = useState(1000)
|
||
const [paperTrades, setPaperTrades] = useState([])
|
||
const [error, setError] = useState(null)
|
||
|
||
// SAFETY: Only these timeframes allowed in paper trading
|
||
const safeTimeframes = [
|
||
{ label: '5m', value: '5', riskLevel: 'EXTREME' },
|
||
{ label: '30m', value: '30', riskLevel: 'HIGH' },
|
||
{ label: '1h', value: '60', riskLevel: 'MEDIUM' },
|
||
{ label: '4h', value: '240', riskLevel: 'LOW' }
|
||
]
|
||
|
||
const settings = {
|
||
riskPerTrade: 1.0,
|
||
paperMode: true, // ALWAYS true - cannot be changed
|
||
isolatedMode: true // ALWAYS true - completely isolated
|
||
}
|
||
|
||
useEffect(() => {
|
||
// Load paper trading data from localStorage
|
||
const savedTrades = localStorage.getItem('safePaperTrades')
|
||
const savedBalance = localStorage.getItem('safePaperBalance')
|
||
|
||
if (savedTrades) {
|
||
setPaperTrades(JSON.parse(savedTrades))
|
||
}
|
||
if (savedBalance) {
|
||
setPaperBalance(parseFloat(savedBalance))
|
||
}
|
||
}, [])
|
||
|
||
// Save to localStorage whenever data changes
|
||
useEffect(() => {
|
||
localStorage.setItem('safePaperTrades', JSON.stringify(paperTrades))
|
||
localStorage.setItem('safePaperBalance', paperBalance.toString())
|
||
}, [paperTrades, paperBalance])
|
||
|
||
const runSafeAnalysis = async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
|
||
try {
|
||
console.log('📄 SAFE PAPER TRADING: Starting isolated analysis...')
|
||
|
||
// SAFETY: Only call the isolated paper trading API
|
||
const response = await fetch('/api/paper-trading-safe', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
symbol,
|
||
timeframe,
|
||
mode: 'PAPER_ONLY', // REQUIRED for safety
|
||
paperTrading: true,
|
||
isolatedMode: true
|
||
})
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Analysis failed: ${response.status}`)
|
||
}
|
||
|
||
const result = await response.json()
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.error || 'Analysis failed')
|
||
}
|
||
|
||
console.log('✅ SAFE ANALYSIS COMPLETE:', result.analysis.recommendation)
|
||
setCurrentAnalysis(result.analysis)
|
||
|
||
} catch (error) {
|
||
console.error('❌ Safe analysis error:', error)
|
||
setError(error.message)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const executeSafePaperTrade = (signal) => {
|
||
if (!currentAnalysis) return
|
||
|
||
const trade = {
|
||
id: Date.now(),
|
||
timestamp: new Date().toISOString(),
|
||
symbol: currentAnalysis.symbol,
|
||
timeframe: currentAnalysis.timeframe,
|
||
side: signal,
|
||
entryPrice: currentAnalysis.entry?.price || 100,
|
||
stopLoss: currentAnalysis.stopLoss?.price,
|
||
takeProfit: currentAnalysis.takeProfits?.tp1?.price,
|
||
confidence: currentAnalysis.confidence,
|
||
reasoning: currentAnalysis.reasoning,
|
||
status: 'OPEN',
|
||
momentumStatus: currentAnalysis.momentumStatus?.type,
|
||
entryQuality: currentAnalysis.entryQuality?.score,
|
||
paperMode: true,
|
||
safeMode: true
|
||
}
|
||
|
||
// Calculate position size based on risk management
|
||
const riskAmount = paperBalance * (settings.riskPerTrade / 100)
|
||
const stopDistance = Math.abs(trade.entryPrice - (trade.stopLoss || trade.entryPrice * 0.95))
|
||
trade.positionSize = Math.min(riskAmount / stopDistance, paperBalance * 0.1)
|
||
|
||
setPaperTrades(prev => [trade, ...prev])
|
||
|
||
console.log('📄 SAFE PAPER TRADE:', trade)
|
||
alert(`✅ Safe Paper Trade: ${signal} ${trade.symbol} at $${trade.entryPrice}\\n💡 This is completely isolated - no real money at risk!`)
|
||
}
|
||
|
||
const closeSafePaperTrade = (tradeId, exitPrice) => {
|
||
setPaperTrades(prev => prev.map(trade => {
|
||
if (trade.id === tradeId && trade.status === 'OPEN') {
|
||
const pnl = trade.side === 'BUY'
|
||
? (exitPrice - trade.entryPrice) * (trade.positionSize / trade.entryPrice)
|
||
: (trade.entryPrice - exitPrice) * (trade.positionSize / trade.entryPrice)
|
||
|
||
setPaperBalance(current => current + pnl)
|
||
|
||
return {
|
||
...trade,
|
||
status: 'CLOSED',
|
||
exitPrice,
|
||
exitTime: new Date().toISOString(),
|
||
pnl,
|
||
exitReason: 'Manual close'
|
||
}
|
||
}
|
||
return trade
|
||
}))
|
||
}
|
||
|
||
const resetSafePaperTrading = () => {
|
||
if (confirm('Reset all SAFE paper trading data? This cannot be undone.')) {
|
||
setPaperBalance(1000)
|
||
setPaperTrades([])
|
||
setCurrentAnalysis(null)
|
||
localStorage.removeItem('safePaperTrades')
|
||
localStorage.removeItem('safePaperBalance')
|
||
}
|
||
}
|
||
|
||
const openTrades = paperTrades.filter(t => t.status === 'OPEN')
|
||
const closedTrades = paperTrades.filter(t => t.status === 'CLOSED')
|
||
const totalPnL = closedTrades.reduce((sum, trade) => sum + (trade.pnl || 0), 0)
|
||
const winRate = closedTrades.length > 0
|
||
? Math.round((closedTrades.filter(t => (t.pnl || 0) > 0).length / closedTrades.length) * 100)
|
||
: 0
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* SAFETY NOTICE */}
|
||
<div className="bg-green-900/30 border border-green-700 rounded-lg p-4">
|
||
<h2 className="text-green-400 font-bold text-lg mb-2">🛡️ SAFE PAPER TRADING MODE</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<p className="text-green-300 mb-1">✅ Completely isolated from real trading</p>
|
||
<p className="text-green-300 mb-1">✅ No connection to live trading APIs</p>
|
||
<p className="text-green-300">✅ Zero risk of real money execution</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-green-300 mb-1">🧠 AI learning through safe simulation</p>
|
||
<p className="text-green-300 mb-1">📊 Mock analysis for practice</p>
|
||
<p className="text-green-300">🎯 Perfect for confidence building</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Header with Balance */}
|
||
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h1 className="text-2xl font-bold text-white">📄 Safe Paper Trading</h1>
|
||
<div className="flex items-center space-x-4">
|
||
<div className="text-right">
|
||
<p className="text-sm text-gray-400">Virtual Balance</p>
|
||
<p className={`text-lg font-bold ${paperBalance >= 1000 ? 'text-green-400' : 'text-red-400'}`}>
|
||
${paperBalance.toFixed(2)}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm text-gray-400">Total P&L</p>
|
||
<p className={`text-lg font-bold ${totalPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||
${totalPnL.toFixed(2)}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-sm text-gray-400">Win Rate</p>
|
||
<p className="text-lg font-bold text-blue-400">{winRate}%</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||
<div className="bg-gray-700/50 rounded p-3 text-center">
|
||
<p className="text-gray-400">Open Trades</p>
|
||
<p className="text-white font-bold text-lg">{openTrades.length}</p>
|
||
</div>
|
||
<div className="bg-gray-700/50 rounded p-3 text-center">
|
||
<p className="text-gray-400">Closed Trades</p>
|
||
<p className="text-white font-bold text-lg">{closedTrades.length}</p>
|
||
</div>
|
||
<div className="bg-gray-700/50 rounded p-3 text-center">
|
||
<p className="text-gray-400">Wins</p>
|
||
<p className="text-green-400 font-bold text-lg">
|
||
{closedTrades.filter(t => (t.pnl || 0) > 0).length}
|
||
</p>
|
||
</div>
|
||
<div className="bg-gray-700/50 rounded p-3 text-center">
|
||
<p className="text-gray-400">Losses</p>
|
||
<p className="text-red-400 font-bold text-lg">
|
||
{closedTrades.filter(t => (t.pnl || 0) < 0).length}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Trading Controls */}
|
||
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
|
||
<h3 className="text-lg font-bold text-white mb-4">🎯 Safe Analysis Controls</h3>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-2">Symbol</label>
|
||
<select
|
||
value={symbol}
|
||
onChange={(e) => setSymbol(e.target.value)}
|
||
className="w-full bg-gray-700 text-white rounded px-3 py-2"
|
||
>
|
||
<option value="SOLUSD">SOL/USD</option>
|
||
<option value="BTCUSD">BTC/USD</option>
|
||
<option value="ETHUSD">ETH/USD</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-2">Timeframe</label>
|
||
<select
|
||
value={timeframe}
|
||
onChange={(e) => setTimeframe(e.target.value)}
|
||
className="w-full bg-gray-700 text-white rounded px-3 py-2"
|
||
>
|
||
{safeTimeframes.map(tf => (
|
||
<option key={tf.value} value={tf.value}>
|
||
{tf.label} ({tf.riskLevel} Risk)
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={runSafeAnalysis}
|
||
disabled={loading}
|
||
className={`w-full py-3 px-4 rounded-lg font-medium transition-all duration-200 ${
|
||
loading
|
||
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||
: 'bg-blue-600 hover:bg-blue-700 text-white'
|
||
}`}
|
||
>
|
||
{loading ? '🔄 Safe Analysis Running...' : '🛡️ Run Safe Paper Analysis'}
|
||
</button>
|
||
|
||
<div className="mt-2 text-xs text-gray-500">
|
||
✅ Isolated mock analysis • No real APIs • Zero trading risk
|
||
</div>
|
||
</div>
|
||
|
||
{/* Error Display */}
|
||
{error && (
|
||
<div className="bg-red-900/30 border border-red-700 rounded-lg p-4">
|
||
<p className="text-red-400">❌ Error: {error}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Current Analysis Results */}
|
||
{currentAnalysis && (
|
||
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
|
||
<h3 className="text-lg font-bold text-white mb-4">📊 Safe Analysis Results</h3>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||
<div className="bg-gray-700/50 rounded p-3">
|
||
<p className="text-gray-400 text-sm">Recommendation</p>
|
||
<p className={`font-bold text-lg ${
|
||
currentAnalysis.recommendation === 'BUY' ? 'text-green-400' :
|
||
currentAnalysis.recommendation === 'SELL' ? 'text-red-400' : 'text-yellow-400'
|
||
}`}>
|
||
{currentAnalysis.recommendation}
|
||
</p>
|
||
</div>
|
||
<div className="bg-gray-700/50 rounded p-3">
|
||
<p className="text-gray-400 text-sm">Confidence</p>
|
||
<p className="font-bold text-lg text-blue-400">{currentAnalysis.confidence}%</p>
|
||
</div>
|
||
<div className="bg-gray-700/50 rounded p-3">
|
||
<p className="text-gray-400 text-sm">Entry Price</p>
|
||
<p className="font-bold text-lg text-white">${currentAnalysis.entry?.price?.toFixed(2)}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
{currentAnalysis.recommendation !== 'HOLD' && currentAnalysis.tradeDecision?.allowed && (
|
||
<div className="flex space-x-4 mb-4">
|
||
<button
|
||
onClick={() => executeSafePaperTrade('BUY')}
|
||
disabled={currentAnalysis.recommendation !== 'BUY'}
|
||
className={`flex-1 py-2 px-4 rounded font-medium ${
|
||
currentAnalysis.recommendation === 'BUY'
|
||
? 'bg-green-600 hover:bg-green-700 text-white'
|
||
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||
}`}
|
||
>
|
||
📄 Safe Paper BUY
|
||
</button>
|
||
<button
|
||
onClick={() => executeSafePaperTrade('SELL')}
|
||
disabled={currentAnalysis.recommendation !== 'SELL'}
|
||
className={`flex-1 py-2 px-4 rounded font-medium ${
|
||
currentAnalysis.recommendation === 'SELL'
|
||
? 'bg-red-600 hover:bg-red-700 text-white'
|
||
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||
}`}
|
||
>
|
||
📄 Safe Paper SELL
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Analysis Details */}
|
||
<div className="bg-gray-700/30 rounded p-4">
|
||
<h4 className="text-white font-medium mb-2">Analysis Reasoning:</h4>
|
||
<pre className="text-gray-300 text-sm whitespace-pre-wrap">
|
||
{currentAnalysis.reasoning}
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Open Trades */}
|
||
{openTrades.length > 0 && (
|
||
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
|
||
<h3 className="text-lg font-bold text-white mb-4">📈 Open Paper Positions</h3>
|
||
<div className="space-y-3">
|
||
{openTrades.map(trade => (
|
||
<div key={trade.id} className="bg-gray-700/50 rounded p-4 flex justify-between items-center">
|
||
<div className="flex-1">
|
||
<div className="flex items-center space-x-4">
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||
trade.side === 'BUY' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'
|
||
}`}>
|
||
{trade.side}
|
||
</span>
|
||
<span className="text-white font-medium">{trade.symbol}</span>
|
||
<span className="text-gray-400">${trade.entryPrice}</span>
|
||
<span className="text-gray-400">Size: ${trade.positionSize?.toFixed(2)}</span>
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-1">
|
||
Entry: {new Date(trade.timestamp).toLocaleString()} |
|
||
Confidence: {trade.confidence}%
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
const exitPrice = prompt(`Exit price for ${trade.symbol}:`, trade.entryPrice)
|
||
if (exitPrice) closeSafePaperTrade(trade.id, parseFloat(exitPrice))
|
||
}}
|
||
className="bg-yellow-600 hover:bg-yellow-700 text-white px-3 py-1 rounded text-sm"
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Trade History */}
|
||
{closedTrades.length > 0 && (
|
||
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-bold text-white">📈 Safe Paper Trade History</h3>
|
||
<button
|
||
onClick={resetSafePaperTrading}
|
||
className="bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm"
|
||
>
|
||
Reset All Data
|
||
</button>
|
||
</div>
|
||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||
{closedTrades.slice(0, 20).map(trade => (
|
||
<div key={trade.id} className={`p-3 rounded border-l-4 ${
|
||
(trade.pnl || 0) >= 0 ? 'bg-green-900/20 border-green-500' : 'bg-red-900/20 border-red-500'
|
||
}`}>
|
||
<div className="flex justify-between items-center">
|
||
<div className="flex items-center space-x-4">
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||
trade.side === 'BUY' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'
|
||
}`}>
|
||
{trade.side}
|
||
</span>
|
||
<span className="text-white">{trade.symbol}</span>
|
||
<span className="text-gray-400">${trade.entryPrice} → ${trade.exitPrice}</span>
|
||
<span className={`font-medium ${
|
||
(trade.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'
|
||
}`}>
|
||
${(trade.pnl || 0).toFixed(2)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-1">
|
||
{new Date(trade.timestamp).toLocaleDateString()} | Confidence: {trade.confidence}%
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|