Files
trading_bot_v4/app/analytics/page.tsx
mindesbunister 056440bf8f feat: add quality score display and timezone fixes
- Add qualityScore to ExecuteTradeResponse interface and response object
- Update analytics page to always show Signal Quality card (N/A if unavailable)
- Fix n8n workflow to pass context metrics and qualityScore to execute endpoint
- Fix timezone in Telegram notifications (Europe/Berlin)
- Fix symbol normalization in /api/trading/close endpoint
- Update Drift ETH-PERP minimum order size (0.002 ETH not 0.01)
- Add transaction confirmation to closePosition() to prevent phantom closes
- Add 30-second grace period for new trades in Position Manager
- Fix execution order: database save before Position Manager.addTrade()
- Update copilot instructions with transaction confirmation pattern
2025-11-01 17:00:37 +01:00

477 lines
21 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.
/**
* Trading Bot v4 - Analytics Dashboard
*
* Comprehensive view of trading performance and statistics
*/
'use client'
import { useState, useEffect } from 'react'
interface Stats {
period: string
realTrades: {
total: number
winning: number
losing: number
winRate: string
totalPnL: string
avgWin: string
avgLoss: string
profitFactor: string
}
testTrades: {
count: number
note: string
}
}
interface LastTrade {
id: string
symbol: string
direction: string
entryPrice: number
entryTime: string
exitPrice?: number
exitTime?: string
exitReason?: string
realizedPnL?: number
realizedPnLPercent?: number
positionSizeUSD: number
leverage: number
stopLossPrice: number
takeProfit1Price: number
takeProfit2Price: number
isTestTrade: boolean
signalQualityScore?: number
}
interface NetPosition {
symbol: string
longUSD: number
shortUSD: number
netUSD: number
netSOL: number
netDirection: 'long' | 'short' | 'flat'
tradeCount: number
}
interface PositionSummary {
summary: {
individualTrades: number
testTrades: number
totalIndividualExposure: string
netExposure: string
explanation: string
}
netPositions: NetPosition[]
}
export default function AnalyticsPage() {
const [stats, setStats] = useState<Stats | null>(null)
const [positions, setPositions] = useState<PositionSummary | null>(null)
const [lastTrade, setLastTrade] = useState<LastTrade | null>(null)
const [loading, setLoading] = useState(true)
const [selectedDays, setSelectedDays] = useState(30)
useEffect(() => {
loadData()
}, [selectedDays])
const loadData = async () => {
setLoading(true)
try {
const [statsRes, positionsRes, lastTradeRes] = await Promise.all([
fetch(`/api/analytics/stats?days=${selectedDays}`),
fetch('/api/analytics/positions'),
fetch('/api/analytics/last-trade'),
])
const statsData = await statsRes.json()
const positionsData = await positionsRes.json()
const lastTradeData = await lastTradeRes.json()
setStats(statsData.stats)
setPositions(positionsData.summary)
setLastTrade(lastTradeData.trade)
} catch (error) {
console.error('Failed to load analytics:', error)
}
setLoading(false)
}
const clearManuallyClosed = async () => {
if (!confirm('Clear all open trades from database? Use this if you manually closed positions in Drift UI.')) {
return
}
try {
const res = await fetch('/api/trading/clear-manual-closes', {
method: 'POST',
})
if (res.ok) {
alert('✅ Manually closed trades cleared from database')
loadData() // Reload data
} else {
const error = await res.json()
alert(`❌ Failed to clear: ${error.error}`)
}
} catch (error) {
console.error('Failed to clear trades:', error)
alert('❌ Failed to clear trades')
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<p className="text-gray-400">Loading analytics...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
{/* Header */}
<div className="bg-gray-800/50 backdrop-blur-sm border-b border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<a href="/" className="text-gray-400 hover:text-white transition">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</a>
<div>
<h1 className="text-2xl font-bold text-white">📊 Analytics Dashboard</h1>
<p className="text-sm text-gray-400">Trading performance and statistics</p>
</div>
</div>
{/* Time Period Selector */}
<div className="flex items-center space-x-4">
<a
href="/analytics/optimization"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white font-semibold transition-colors"
>
🎯 TP/SL Optimization
</a>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-400">Period:</span>
<select
value={selectedDays}
onChange={(e) => setSelectedDays(Number(e.target.value))}
className="bg-gray-700 text-white rounded-lg px-4 py-2 border border-gray-600 focus:border-blue-500 focus:outline-none"
>
<option value={7}>7 days</option>
<option value={30}>30 days</option>
<option value={90}>90 days</option>
<option value={365}>1 year</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Position Summary */}
{positions && (
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-white">📍 Current Positions</h2>
<button
onClick={clearManuallyClosed}
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg text-white text-sm font-semibold transition-colors"
title="Clear open trades from database if you manually closed them in Drift UI"
>
🗑 Clear Manual Closes
</button>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Open Trades</div>
<div className="text-3xl font-bold text-white">{positions.summary.individualTrades}</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Test Trades</div>
<div className="text-3xl font-bold text-yellow-500">{positions.summary.testTrades}</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Total Exposure</div>
<div className="text-3xl font-bold text-blue-400">{positions.summary.totalIndividualExposure}</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Net Exposure</div>
<div className="text-3xl font-bold text-purple-400">{positions.summary.netExposure}</div>
</div>
</div>
{positions.netPositions.length > 0 && (
<div className="mt-4 bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<h3 className="text-lg font-semibold text-white mb-4">Net Positions (Drift View)</h3>
<div className="space-y-3">
{positions.netPositions.map((pos, i) => (
<div key={i} className="flex items-center justify-between p-4 bg-gray-700/30 rounded-lg">
<div className="flex items-center space-x-4">
<div className="text-2xl">🎯</div>
<div>
<div className="text-white font-medium">{pos.symbol}</div>
<div className="text-sm text-gray-400">{pos.tradeCount} trade{pos.tradeCount > 1 ? 's' : ''}</div>
</div>
</div>
<div className="text-right">
<div className={`text-lg font-bold ${pos.netDirection === 'long' ? 'text-green-400' : pos.netDirection === 'short' ? 'text-red-400' : 'text-gray-400'}`}>
{pos.netDirection.toUpperCase()}: {Math.abs(pos.netSOL).toFixed(4)} SOL
</div>
<div className="text-sm text-gray-400">${Math.abs(pos.netUSD).toFixed(2)}</div>
</div>
</div>
))}
</div>
</div>
)}
{positions.summary.individualTrades === 0 && (
<div className="mt-4 bg-gray-800/30 backdrop-blur-sm rounded-xl p-8 border border-gray-700 text-center">
<div className="text-4xl mb-2">📭</div>
<p className="text-gray-400">No open positions</p>
</div>
)}
</div>
)}
{/* Last Trade Details */}
{lastTrade && (
<div className="mb-8">
<h2 className="text-xl font-bold text-white mb-4">🔍 Last Trade</h2>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-4">
<div className="text-3xl">
{lastTrade.direction === 'long' ? '📈' : '📉'}
</div>
<div>
<div className="text-2xl font-bold text-white">{lastTrade.symbol}</div>
<div className="flex items-center space-x-2 mt-1">
<span className={`px-3 py-1 rounded-full text-sm font-semibold ${lastTrade.direction === 'long' ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400'}`}>
{lastTrade.direction.toUpperCase()}
</span>
{lastTrade.isTestTrade && (
<span className="px-3 py-1 rounded-full text-sm font-semibold bg-yellow-900/50 text-yellow-400">
TEST
</span>
)}
</div>
</div>
</div>
{lastTrade.exitTime && lastTrade.realizedPnL !== undefined && (
<div className="text-right">
<div className={`text-3xl font-bold ${lastTrade.realizedPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{lastTrade.realizedPnL >= 0 ? '+' : ''}${lastTrade.realizedPnL.toFixed(2)}
</div>
{lastTrade.realizedPnLPercent !== undefined && (
<div className={`text-sm ${lastTrade.realizedPnLPercent >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{lastTrade.realizedPnLPercent >= 0 ? '+' : ''}{lastTrade.realizedPnLPercent.toFixed(2)}%
</div>
)}
</div>
)}
{!lastTrade.exitTime && (
<div className="text-right">
<div className="text-2xl font-bold text-blue-400">OPEN</div>
<div className="text-sm text-gray-400">Currently active</div>
</div>
)}
</div>
<div className="grid md:grid-cols-3 gap-4 mb-4">
<div className="bg-gray-700/30 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Entry</div>
<div className="text-xl font-bold text-white">${lastTrade.entryPrice.toFixed(4)}</div>
<div className="text-xs text-gray-500">
{new Date(lastTrade.entryTime).toLocaleString()}
</div>
</div>
<div className="bg-gray-700/30 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Position Size</div>
<div className="text-xl font-bold text-white">${lastTrade.positionSizeUSD.toFixed(2)}</div>
<div className="text-xs text-gray-500">
{lastTrade.leverage}x leverage
</div>
</div>
<div className="bg-gray-700/30 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Signal Quality</div>
{lastTrade.signalQualityScore !== undefined ? (
<>
<div className={`text-xl font-bold ${lastTrade.signalQualityScore >= 80 ? 'text-green-400' : lastTrade.signalQualityScore >= 70 ? 'text-yellow-400' : 'text-orange-400'}`}>
{lastTrade.signalQualityScore}/100
</div>
<div className="text-xs text-gray-500">
{lastTrade.signalQualityScore >= 80 ? 'Excellent' : lastTrade.signalQualityScore >= 70 ? 'Good' : 'Marginal'}
</div>
</>
) : (
<>
<div className="text-xl font-bold text-gray-500">N/A</div>
<div className="text-xs text-gray-500">No score available</div>
</>
)}
</div>
</div>
{lastTrade.exitTime && lastTrade.exitPrice && (
<div className="grid md:grid-cols-1 gap-4 mb-4">
<div className="bg-gray-700/30 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-1">Exit</div>
<div className="text-xl font-bold text-white">${lastTrade.exitPrice.toFixed(4)}</div>
<div className="text-xs text-gray-500">
{new Date(lastTrade.exitTime).toLocaleString()}
</div>
</div>
</div>
)}
<div className="grid md:grid-cols-3 gap-4">
<div className="bg-gray-700/30 rounded-lg p-3">
<div className="text-xs text-gray-400 mb-1">Stop Loss</div>
<div className="text-lg font-semibold text-red-400">${lastTrade.stopLossPrice.toFixed(4)}</div>
</div>
<div className="bg-gray-700/30 rounded-lg p-3">
<div className="text-xs text-gray-400 mb-1">TP1</div>
<div className="text-lg font-semibold text-green-400">${lastTrade.takeProfit1Price.toFixed(4)}</div>
</div>
<div className="bg-gray-700/30 rounded-lg p-3">
<div className="text-xs text-gray-400 mb-1">TP2</div>
<div className="text-lg font-semibold text-green-400">${lastTrade.takeProfit2Price.toFixed(4)}</div>
</div>
</div>
{lastTrade.exitReason && (
<div className="mt-4 p-3 bg-blue-900/20 rounded-lg border border-blue-500/30">
<span className="text-sm text-gray-400">Exit Reason: </span>
<span className="text-sm font-semibold text-blue-400">{lastTrade.exitReason}</span>
</div>
)}
</div>
</div>
)}
{/* Trading Statistics */}
{stats && (
<div>
<h2 className="text-xl font-bold text-white mb-4">📈 Performance ({stats.period})</h2>
{/* Main Stats Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Total Trades</div>
<div className="text-3xl font-bold text-white">{stats.realTrades.total}</div>
<div className="text-xs text-gray-500 mt-2">
{stats.testTrades.count} test trade{stats.testTrades.count !== 1 ? 's' : ''} excluded
</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Win Rate</div>
<div className="text-3xl font-bold text-green-400">{stats.realTrades.winRate}</div>
<div className="text-xs text-gray-500 mt-2">
{stats.realTrades.winning}W / {stats.realTrades.losing}L
</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Total P&L</div>
<div className={`text-3xl font-bold ${parseFloat(stats.realTrades.totalPnL.replace('$', '')) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{stats.realTrades.totalPnL}
</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<div className="text-sm text-gray-400 mb-1">Profit Factor</div>
<div className="text-3xl font-bold text-blue-400">{stats.realTrades.profitFactor}</div>
</div>
</div>
{/* Detailed Stats */}
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<span className="text-2xl mr-2"></span>
Winning Trades
</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-400">Count:</span>
<span className="text-white font-medium">{stats.realTrades.winning}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Average Win:</span>
<span className="text-green-400 font-medium">{stats.realTrades.avgWin}</span>
</div>
</div>
</div>
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<span className="text-2xl mr-2"></span>
Losing Trades
</h3>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-400">Count:</span>
<span className="text-white font-medium">{stats.realTrades.losing}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Average Loss:</span>
<span className="text-red-400 font-medium">{stats.realTrades.avgLoss}</span>
</div>
</div>
</div>
</div>
{stats.realTrades.total === 0 && (
<div className="mt-6 bg-gray-800/30 backdrop-blur-sm rounded-xl p-8 border border-gray-700 text-center">
<div className="text-4xl mb-2">📊</div>
<p className="text-gray-400 mb-2">No trading data yet</p>
<p className="text-sm text-gray-500">Start trading to see your performance statistics</p>
</div>
)}
</div>
)}
{/* Info Note */}
<div className="mt-8 bg-blue-900/20 backdrop-blur-sm rounded-xl p-6 border border-blue-500/30">
<div className="flex items-start space-x-3">
<div className="text-2xl">💡</div>
<div>
<h4 className="text-white font-semibold mb-2">Understanding Position Netting</h4>
<p className="text-gray-300 text-sm leading-relaxed">
Drift perpetual futures automatically NET opposite positions in the same market.
If you have both LONG and SHORT positions in SOL-PERP, Drift shows only the net exposure.
The database tracks individual trades for complete history, while this dashboard shows
your actual market exposure.
</p>
</div>
</div>
</div>
</div>
</div>
)
}