782 lines
39 KiB
HTML
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>
|