tradusai/web/static/index.html
2025-12-09 12:27:47 +08:00

782 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
success: '#10b981',
danger: '#ef4444',
warning: '#f59e0b',
}
}
}
}
</script>
<style>
body {
font-family: 'Inter', system-ui, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
min-height: 100vh;
}
.glass-card {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.stat-card {
background: linear-gradient(145deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.9));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
.glow-success {
box-shadow: 0 0 20px rgba(16, 185, 129, 0.3);
}
.glow-danger {
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
}
.glow-primary {
box-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
}
.text-gradient {
background: linear-gradient(135deg, #38bdf8, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.progress-ring {
transform: rotate(-90deg);
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge-long {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.badge-short {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.badge-hold {
background: rgba(148, 163, 184, 0.2);
color: #94a3b8;
border: 1px solid rgba(148, 163, 184, 0.3);
}
.badge-flat {
background: rgba(100, 116, 139, 0.2);
color: #64748b;
border: 1px solid rgba(100, 116, 139, 0.3);
}
.table-row {
transition: background 0.2s;
}
.table-row:hover {
background: rgba(255, 255, 255, 0.05);
}
.pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.1); }
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(30, 41, 59, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.7);
}
</style>
</head>
<body class="text-slate-100">
<div class="container mx-auto px-4 py-8 max-w-7xl">
<!-- Header -->
<header class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<div>
<h1 class="text-3xl md:text-4xl font-bold text-gradient mb-2">Trading Dashboard</h1>
<p class="text-slate-400 text-sm">BTC/USDT Perpetual</p>
</div>
<div class="flex items-center gap-4">
<div id="connection-status" class="flex items-center gap-2 px-4 py-2 rounded-full bg-yellow-500/20 text-yellow-400 text-sm font-medium">
<span class="w-2 h-2 rounded-full bg-yellow-400 pulse-dot"></span>
Connecting...
</div>
<div class="text-slate-500 text-sm">
<span id="last-update">--:--:--</span>
</div>
</div>
</header>
<!-- Main Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<!-- Balance -->
<div class="stat-card p-5 fade-in">
<div class="flex items-center gap-2 mb-3">
<div class="w-10 h-10 rounded-xl bg-primary-500/20 flex items-center justify-center">
<svg class="w-5 h-5 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<span class="text-slate-400 text-sm font-medium">Balance</span>
</div>
<div id="balance" class="text-2xl md:text-3xl font-bold text-white mb-1">$10,000.00</div>
<div id="total-return" class="text-sm font-medium text-slate-400">+0.00%</div>
</div>
<!-- Total Trades -->
<div class="stat-card p-5 fade-in" style="animation-delay: 0.1s">
<div class="flex items-center gap-2 mb-3">
<div class="w-10 h-10 rounded-xl bg-purple-500/20 flex items-center justify-center">
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
<span class="text-slate-400 text-sm font-medium">Total Trades</span>
</div>
<div id="total-trades" class="text-2xl md:text-3xl font-bold text-white mb-1">0</div>
<div class="flex items-center gap-2 text-sm">
<span class="text-success" id="winning-trades">0</span>
<span class="text-slate-500">/</span>
<span class="text-danger" id="losing-trades">0</span>
</div>
</div>
<!-- Win Rate -->
<div class="stat-card p-5 fade-in" style="animation-delay: 0.2s">
<div class="flex items-center gap-2 mb-3">
<div class="w-10 h-10 rounded-xl bg-success/20 flex items-center justify-center">
<svg class="w-5 h-5 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
</div>
<span class="text-slate-400 text-sm font-medium">Win Rate</span>
</div>
<div id="win-rate" class="text-2xl md:text-3xl font-bold text-white mb-1">0.0%</div>
<div class="text-sm text-slate-400">PF: <span id="profit-factor">0.00</span></div>
</div>
<!-- Total PnL -->
<div class="stat-card p-5 fade-in" style="animation-delay: 0.3s">
<div class="flex items-center gap-2 mb-3">
<div class="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center">
<svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
<span class="text-slate-400 text-sm font-medium">Total PnL</span>
</div>
<div id="total-pnl" class="text-2xl md:text-3xl font-bold text-white mb-1">$0.00</div>
<div class="text-sm text-slate-400">Max DD: <span id="max-drawdown" class="text-danger">0.00%</span></div>
</div>
</div>
<!-- Position & Signal Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Current Position -->
<div class="glass-card p-6 fade-in" style="animation-delay: 0.4s">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Current Position
</h2>
<span id="position-badge" class="badge badge-flat">FLAT</span>
</div>
<div id="position-info">
<div class="flex flex-col items-center justify-center py-12 text-slate-500">
<svg class="w-16 h-16 mb-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 12H4M12 4v16"/>
</svg>
<p class="text-lg font-medium">No Position</p>
<p class="text-sm text-slate-600">Waiting for signal...</p>
</div>
</div>
</div>
<!-- Latest Signal -->
<div class="glass-card p-6 fade-in" style="animation-delay: 0.5s">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
Latest Signal
</h2>
<span id="signal-badge" class="badge badge-hold">HOLD</span>
</div>
<div id="signal-info">
<div class="flex flex-col items-center justify-center py-12 text-slate-500">
<svg class="w-16 h-16 mb-4 text-slate-600 animate-spin" style="animation-duration: 3s" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<p class="text-lg font-medium">Loading Signal</p>
<p class="text-sm text-slate-600">Fetching latest analysis...</p>
</div>
</div>
</div>
</div>
<!-- Equity Chart -->
<div class="glass-card p-6 mb-8 fade-in" style="animation-delay: 0.6s">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/>
</svg>
Equity Curve
</h2>
<div class="flex items-center gap-4 text-sm">
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-primary-400"></span>
<span class="text-slate-400">Equity</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-success"></span>
<span class="text-slate-400">Balance</span>
</div>
</div>
</div>
<div class="h-72">
<canvas id="equity-chart"></canvas>
</div>
</div>
<!-- Recent Trades -->
<div class="glass-card p-6 fade-in" style="animation-delay: 0.7s">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
<svg class="w-5 h-5 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
</svg>
Trade History
</h2>
<span id="trade-count" class="text-sm text-slate-500">0 trades</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-slate-400 text-xs uppercase tracking-wider border-b border-slate-700/50">
<th class="px-4 py-3 text-left font-medium">ID</th>
<th class="px-4 py-3 text-left font-medium">Side</th>
<th class="px-4 py-3 text-right font-medium">Entry</th>
<th class="px-4 py-3 text-right font-medium">Exit</th>
<th class="px-4 py-3 text-right font-medium">Size</th>
<th class="px-4 py-3 text-right font-medium">PnL</th>
<th class="px-4 py-3 text-left font-medium">Reason</th>
<th class="px-4 py-3 text-left font-medium">Time</th>
</tr>
</thead>
<tbody id="trades-table" class="divide-y divide-slate-700/30">
<tr>
<td colspan="8" class="text-center py-12 text-slate-500">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<p>No trades yet</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Footer -->
<footer class="mt-8 text-center text-slate-600 text-sm">
<p>Trading Dashboard</p>
</footer>
</div>
<script>
// Chart instance
let equityChart = null;
// WebSocket connection
let ws = null;
let reconnectInterval = null;
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
ws.onopen = () => {
const statusEl = document.getElementById('connection-status');
statusEl.className = 'flex items-center gap-2 px-4 py-2 rounded-full bg-success/20 text-success text-sm font-medium';
statusEl.innerHTML = '<span class="w-2 h-2 rounded-full bg-success"></span> Connected';
if (reconnectInterval) {
clearInterval(reconnectInterval);
reconnectInterval = null;
}
};
ws.onclose = () => {
const statusEl = document.getElementById('connection-status');
statusEl.className = 'flex items-center gap-2 px-4 py-2 rounded-full bg-danger/20 text-danger text-sm font-medium';
statusEl.innerHTML = '<span class="w-2 h-2 rounded-full bg-danger pulse-dot"></span> Disconnected';
if (!reconnectInterval) {
reconnectInterval = setInterval(() => {
console.log('Reconnecting...');
connectWebSocket();
}, 3000);
}
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
function handleMessage(data) {
if (data.type === 'init') {
updateState(data.state);
updateSignal(data.signal);
} else if (data.type === 'state_update') {
updateState(data.state);
} else if (data.type === 'signal_update') {
updateSignal(data.signal);
}
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
}
function updateState(state) {
if (!state) return;
const balance = state.balance || 10000;
const initialBalance = 10000;
const totalReturn = ((balance - initialBalance) / initialBalance) * 100;
document.getElementById('balance').textContent = `$${balance.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
const returnEl = document.getElementById('total-return');
returnEl.textContent = `${totalReturn >= 0 ? '+' : ''}${totalReturn.toFixed(2)}%`;
returnEl.className = `text-sm font-medium ${totalReturn > 0 ? 'text-success' : totalReturn < 0 ? 'text-danger' : 'text-slate-400'}`;
const stats = state.stats || {};
document.getElementById('total-trades').textContent = stats.total_trades || 0;
document.getElementById('winning-trades').textContent = stats.winning_trades || 0;
document.getElementById('losing-trades').textContent = stats.losing_trades || 0;
document.getElementById('win-rate').textContent = `${(stats.win_rate || 0).toFixed(1)}%`;
document.getElementById('profit-factor').textContent = (stats.profit_factor || 0).toFixed(2);
const totalPnl = stats.total_pnl || 0;
const pnlEl = document.getElementById('total-pnl');
pnlEl.textContent = `${totalPnl >= 0 ? '+' : ''}$${Math.abs(totalPnl).toFixed(2)}`;
pnlEl.className = `text-2xl md:text-3xl font-bold mb-1 ${totalPnl > 0 ? 'text-success' : totalPnl < 0 ? 'text-danger' : 'text-white'}`;
document.getElementById('max-drawdown').textContent = `${(stats.max_drawdown || 0).toFixed(2)}%`;
updatePosition(state.position);
updateTrades(state.trades || []);
updateEquityChart(state.equity_curve || []);
}
function updatePosition(position) {
const container = document.getElementById('position-info');
const badge = document.getElementById('position-badge');
if (!position || position.side === 'FLAT' || !position.total_size || position.total_size <= 0) {
badge.className = 'badge badge-flat';
badge.textContent = 'FLAT';
container.innerHTML = `
<div class="flex flex-col items-center justify-center py-12 text-slate-500">
<svg class="w-16 h-16 mb-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 12H4M12 4v16"/>
</svg>
<p class="text-lg font-medium">No Position</p>
<p class="text-sm text-slate-600">Waiting for signal...</p>
</div>
`;
return;
}
const isLong = position.side === 'LONG';
badge.className = isLong ? 'badge badge-long' : 'badge badge-short';
badge.textContent = position.side;
const unrealizedPnl = position.unrealized_pnl || 0;
const unrealizedPct = position.unrealized_pnl_pct || 0;
const pnlColor = unrealizedPnl >= 0 ? 'text-success' : 'text-danger';
const entries = position.entries || [];
container.innerHTML = `
<div class="grid grid-cols-2 gap-4">
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-500 text-xs uppercase tracking-wider mb-1">Avg Entry</div>
<div class="text-xl font-bold text-white font-mono">$${(position.avg_entry_price || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
</div>
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-500 text-xs uppercase tracking-wider mb-1">Size</div>
<div class="text-xl font-bold text-white font-mono">${(position.total_size || 0).toFixed(6)}</div>
<div class="text-xs text-slate-500">${entries.length} entries</div>
</div>
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-500 text-xs uppercase tracking-wider mb-1">Stop Loss</div>
<div class="text-lg font-bold text-danger font-mono">$${(position.stop_loss || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
</div>
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-500 text-xs uppercase tracking-wider mb-1">Take Profit</div>
<div class="text-lg font-bold text-success font-mono">$${(position.take_profit || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
</div>
</div>
${position.current_price ? `
<div class="mt-4 p-4 rounded-xl ${unrealizedPnl >= 0 ? 'bg-success/10 border border-success/20' : 'bg-danger/10 border border-danger/20'}">
<div class="flex justify-between items-center">
<span class="text-slate-400 text-sm">Unrealized PnL</span>
<span class="text-xl font-bold ${pnlColor} font-mono">
${unrealizedPnl >= 0 ? '+' : ''}$${Math.abs(unrealizedPnl).toFixed(2)}
<span class="text-sm">(${unrealizedPct >= 0 ? '+' : ''}${unrealizedPct.toFixed(2)}%)</span>
</span>
</div>
</div>
` : ''}
`;
}
function updateSignal(signal) {
const container = document.getElementById('signal-info');
const badge = document.getElementById('signal-badge');
if (!signal || !signal.aggregated_signal) {
badge.className = 'badge badge-hold';
badge.textContent = 'LOADING';
container.innerHTML = `
<div class="flex flex-col items-center justify-center py-12 text-slate-500">
<svg class="w-16 h-16 mb-4 text-slate-600 animate-spin" style="animation-duration: 3s" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<p class="text-lg font-medium">Loading Signal</p>
<p class="text-sm text-slate-600">Fetching latest analysis...</p>
</div>
`;
return;
}
const agg = signal.aggregated_signal;
const llm = agg.llm_signal || {};
const price = agg.levels?.current_price || signal.market_analysis?.price || 0;
const signalType = agg.final_signal || 'HOLD';
const confidence = (agg.final_confidence || 0) * 100;
badge.className = signalType === 'LONG' ? 'badge badge-long' : signalType === 'SHORT' ? 'badge badge-short' : 'badge badge-hold';
badge.textContent = signalType;
const shortTerm = llm.opportunities?.short_term_5m_15m_1h || llm.opportunities?.intraday || {};
const hasOpportunity = shortTerm.exists;
container.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-500 text-xs uppercase tracking-wider mb-1">Price</div>
<div class="text-xl font-bold text-white font-mono">$${price.toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
</div>
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-500 text-xs uppercase tracking-wider mb-1">Confidence</div>
<div class="text-xl font-bold text-white">${confidence.toFixed(0)}%</div>
<div class="w-full bg-slate-700 rounded-full h-1.5 mt-2">
<div class="bg-primary-500 h-1.5 rounded-full transition-all" style="width: ${confidence}%"></div>
</div>
</div>
</div>
<div class="p-4 rounded-xl ${hasOpportunity ? (shortTerm.direction === 'LONG' ? 'bg-success/10 border border-success/20' : 'bg-danger/10 border border-danger/20') : 'bg-slate-800/50'}">
<div class="text-slate-400 text-xs uppercase tracking-wider mb-2">Short-term Opportunity</div>
${hasOpportunity ? `
<div class="flex items-center gap-2 mb-2">
<span class="badge ${shortTerm.direction === 'LONG' ? 'badge-long' : 'badge-short'}">${shortTerm.direction}</span>
</div>
<div class="grid grid-cols-3 gap-2 text-sm">
<div>
<div class="text-slate-500 text-xs">Entry</div>
<div class="text-white font-mono">$${shortTerm.entry_price?.toFixed(0) || '-'}</div>
</div>
<div>
<div class="text-slate-500 text-xs">SL</div>
<div class="text-danger font-mono">$${shortTerm.stop_loss?.toFixed(0) || '-'}</div>
</div>
<div>
<div class="text-slate-500 text-xs">TP</div>
<div class="text-success font-mono">$${shortTerm.take_profit?.toFixed(0) || '-'}</div>
</div>
</div>
` : `
<div class="text-sm text-slate-500">${shortTerm.reasoning || 'No opportunity available'}</div>
`}
</div>
${llm.reasoning ? `
<div class="bg-slate-800/50 rounded-xl p-4">
<div class="text-slate-400 text-xs uppercase tracking-wider mb-2">Analysis</div>
<div class="text-sm text-slate-300 leading-relaxed line-clamp-3">${llm.reasoning}</div>
</div>
` : ''}
</div>
`;
}
function updateTrades(trades) {
const tbody = document.getElementById('trades-table');
document.getElementById('trade-count').textContent = `${trades.length} trades`;
if (!trades || trades.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-12 text-slate-500">
<svg class="w-12 h-12 mx-auto mb-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<p>No trades yet</p>
</td>
</tr>
`;
return;
}
const recentTrades = trades.slice(-20).reverse();
tbody.innerHTML = recentTrades.map(trade => {
const pnl = trade.pnl || 0;
const pnlPct = trade.pnl_pct || 0;
const isWin = pnl > 0;
return `
<tr class="table-row">
<td class="px-4 py-3 text-slate-400 font-mono text-xs">${trade.id}</td>
<td class="px-4 py-3">
<span class="badge ${trade.side === 'LONG' ? 'badge-long' : 'badge-short'}">${trade.side}</span>
</td>
<td class="px-4 py-3 text-right font-mono text-white">$${(trade.entry_price || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</td>
<td class="px-4 py-3 text-right font-mono text-white">$${(trade.exit_price || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</td>
<td class="px-4 py-3 text-right font-mono text-slate-400">${(trade.size || 0).toFixed(6)}</td>
<td class="px-4 py-3 text-right">
<span class="${isWin ? 'text-success' : 'text-danger'} font-mono font-medium">
${pnl >= 0 ? '+' : ''}$${Math.abs(pnl).toFixed(2)}
</span>
<span class="text-xs ${isWin ? 'text-success/70' : 'text-danger/70'} ml-1">
(${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)
</span>
</td>
<td class="px-4 py-3 text-slate-400 text-xs">${trade.exit_reason || '-'}</td>
<td class="px-4 py-3 text-slate-500 text-xs">${formatTime(trade.exit_time)}</td>
</tr>
`;
}).join('');
}
function updateEquityChart(equityData) {
const ctx = document.getElementById('equity-chart').getContext('2d');
if (!equityData || equityData.length === 0) {
if (equityChart) {
equityChart.destroy();
equityChart = null;
}
return;
}
const labels = equityData.map(d => formatTime(d.timestamp));
const equity = equityData.map(d => d.equity);
const balance = equityData.map(d => d.balance);
if (equityChart) {
equityChart.data.labels = labels;
equityChart.data.datasets[0].data = equity;
equityChart.data.datasets[1].data = balance;
equityChart.update('none');
} else {
equityChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Equity',
data: equity,
borderColor: '#0ea5e9',
backgroundColor: 'rgba(14, 165, 233, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 0,
borderWidth: 2,
},
{
label: 'Balance',
data: balance,
borderColor: '#10b981',
borderDash: [5, 5],
fill: false,
tension: 0.4,
pointRadius: 0,
borderWidth: 2,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: 'rgba(15, 23, 42, 0.9)',
titleColor: '#f1f5f9',
bodyColor: '#cbd5e1',
borderColor: 'rgba(100, 116, 139, 0.3)',
borderWidth: 1,
padding: 12,
cornerRadius: 8,
displayColors: true,
callbacks: {
label: function(context) {
return `${context.dataset.label}: $${context.parsed.y.toFixed(2)}`;
}
}
}
},
scales: {
x: {
display: true,
grid: {
color: 'rgba(100, 116, 139, 0.1)',
drawBorder: false,
},
ticks: {
color: '#64748b',
maxTicksLimit: 8,
font: {
size: 11,
}
}
},
y: {
display: true,
grid: {
color: 'rgba(100, 116, 139, 0.1)',
drawBorder: false,
},
ticks: {
color: '#64748b',
callback: function(value) {
return '$' + value.toLocaleString();
},
font: {
size: 11,
}
}
}
}
}
});
}
}
function formatTime(isoString) {
if (!isoString) return '-';
const date = new Date(isoString);
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
}
async function loadInitialData() {
try {
const [statusRes, tradesRes, equityRes, signalRes] = await Promise.all([
fetch('/api/status'),
fetch('/api/trades?limit=50'),
fetch('/api/equity?limit=500'),
fetch('/api/signal'),
]);
const status = await statusRes.json();
const trades = await tradesRes.json();
const equity = await equityRes.json();
const signal = await signalRes.json();
updateState({
balance: status.balance,
position: status.position,
stats: status.stats,
trades: trades.trades,
equity_curve: equity.data,
});
updateSignal({ aggregated_signal: signal });
} catch (error) {
console.error('Error loading initial data:', error);
}
}
document.addEventListener('DOMContentLoaded', () => {
loadInitialData();
connectWebSocket();
});
</script>
</body>
</html>