Files
trading-daemon/dashboard/public/index.html
Melchior Reimers dbd4fbfb47
All checks were successful
Deployment / deploy-docker (push) Successful in 5s
fix: date parsing, continent grouping, dynamic analytics, and sector scraping
2026-01-23 18:17:02 +01:00

410 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading Intelligence 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=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Outfit', sans-serif;
background-color: #0b1120;
color: #e2e8f0;
}
.glass {
background: rgba(30, 41, 59, 0.4);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
.glass-btn {
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.2);
transition: all 0.3s;
}
.glass-btn:hover {
background: rgba(56, 189, 248, 0.2);
border-color: #38bdf8;
}
.glow {
box-shadow: 0 0 20px rgba(56, 189, 248, 0.1);
}
.active-nav {
background: rgba(56, 189, 248, 0.1);
color: #38bdf8;
border-right: 3px solid #38bdf8;
}
canvas {
max-width: 100%;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 10px;
}
</style>
</head>
<body class="flex min-h-screen overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 glass m-4 mr-0 flex flex-col hidden lg:flex">
<div class="p-6">
<h1 class="text-2xl font-bold bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text text-transparent">
Antigravity Trade</h1>
</div>
<nav class="flex-1 px-4 space-y-1 mt-4" id="sidebar">
<a href="#" onclick="showView('dashboard')" id="nav-dashboard"
class="flex items-center p-3 rounded-xl transition active-nav">
<span class="mr-3">📊</span> Dashboard
</a>
<a href="#" onclick="showView('analytics')" id="nav-analytics"
class="flex items-center p-3 rounded-xl transition">
<span class="mr-3">📈</span> Analytics Hub
</a>
<a href="#" onclick="showView('metadata')" id="nav-metadata"
class="flex items-center p-3 rounded-xl transition">
<span class="mr-3">🏢</span> Companies
</a>
</nav>
<div class="p-6 mt-auto">
<div class="glass p-4 text-xs text-sky-400 border-sky-500/30">
Data Stream: <span class="text-green-400 animate-pulse">● Live</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 p-8 overflow-y-auto">
<header class="flex justify-between items-center mb-10">
<div id="viewHeader">
<h2 class="text-3xl font-bold">Market Intelligence</h2>
<p class="text-slate-400 mt-1">Cross-exchange analysis and real-time visualization.</p>
</div>
<div class="flex items-center space-x-4">
<div id="activeIsins" class="flex -space-x-2">
<!-- Pinned ISINs will show up here -->
</div>
<select id="timeRange" class="glass px-4 py-2 text-sm outline-none focus:ring-1 ring-sky-500"
onchange="fetchData()">
<option value="1">Last 24 Hours</option>
<option value="7" selected>Last 7 Days</option>
<option value="30">Last 30 Days</option>
<option value="90">Last Quarter</option>
</select>
<button onclick="fetchData()" class="glass p-2 px-4 text-sky-400 hover:text-sky-300"></button>
</div>
</header>
<!-- DASHBOARD VIEW -->
<div id="view-dashboard" class="view">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div class="glass p-6 glow border-l-4 border-sky-500">
<p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Transaction Volume</p>
<h3 id="statVolume" class="text-4xl font-bold">€0.0k</h3>
</div>
<div class="glass p-6 border-l-4 border-amber-500">
<p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Total Executions</p>
<h3 id="statTrades" class="text-4xl font-bold">0</h3>
</div>
<div class="glass p-6 border-l-4 border-emerald-500">
<p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Unique Assets</p>
<h3 id="statIsins" class="text-4xl font-bold">0</h3>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8 mb-10">
<div class="glass p-8">
<h3 class="text-xl font-bold mb-6">Execution Price Trend</h3>
<div class="h-80"><canvas id="priceChart"></canvas></div>
</div>
<div class="glass p-8">
<h3 class="text-xl font-bold mb-6">Geographic Distribution</h3>
<div class="h-80"><canvas id="continentChart"></canvas></div>
</div>
</div>
</div>
<!-- ANALYTICS VIEW -->
<div id="view-analytics" class="view hidden">
<div class="glass p-8 mb-8">
<div class="flex justify-between items-center mb-8">
<h3 class="text-xl font-bold">Custom Feature Comparison</h3>
<div class="flex space-x-4">
<select id="axisX" class="glass px-3 py-1 text-xs" onchange="renderCustomChart()">
<option value="time">Time</option>
<option value="exchange">Exchange</option>
<option value="continent">Continent</option>
</select>
<select id="axisY" class="glass px-3 py-1 text-xs" onchange="renderCustomChart()">
<option value="volume">Trade Volume</option>
<option value="count">Trade Count</option>
<option value="avg_price">Avg. Price</option>
</select>
</div>
</div>
<div class="h-96"><canvas id="customChart"></canvas></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="glass p-6">
<h3 class="text-lg font-bold mb-6">Exchange Dominance</h3>
<div class="h-64"><canvas id="exchangeChart"></canvas></div>
</div>
<div class="glass p-6">
<h3 class="text-lg font-bold mb-6">Market Dynamics</h3>
<div id="dynamicStats" class="space-y-4">
<!-- Stats injected here -->
</div>
</div>
</div>
</div>
<!-- COMPANIES VIEW -->
<div id="view-metadata" class="view hidden">
<div class="glass overflow-hidden">
<div class="p-6 border-b border-white/5 flex justify-between items-center">
<h3 class="text-xl font-bold text-sky-400">ISIN Metadata Directory</h3>
<input type="text" id="metadataSearch" onkeyup="filterMetadata()"
placeholder="Search ISIN, Name or Sector..."
class="glass px-4 py-2 text-sm w-80 outline-none focus:border-sky-500">
</div>
<table class="w-full text-left">
<thead class="bg-white/5 text-slate-400 text-xs uppercase tracking-tighter font-bold">
<tr>
<th class="p-4 px-6">ISIN</th>
<th class="p-4">Entity Legal Name</th>
<th class="p-4">Origin</th>
<th class="p-4">Continent</th>
<th class="p-4">Market Sector</th>
<th class="p-4 text-center">Actions</th>
</tr>
</thead>
<tbody id="metadataRows" class="divide-y divide-white/5 text-sm">
<!-- Filled by JS -->
</tbody>
</table>
</div>
</div>
</main>
<script>
const API = '/api';
let store = { trades: [], metadata: [], summary: [], comparison: [], pinnedIsins: [] };
let charts = {};
async function fetchData() {
try {
const days = document.getElementById('timeRange').value;
const [t, m, s, c] = await Promise.all([
fetch(`${API}/trades?days=${days}`).then(r => r.json()),
fetch(`${API}/metadata`).then(r => r.json()),
fetch(`${API}/summary`).then(r => r.json()),
fetch(`${API}/comparison?days=${days}`).then(r => r.json())
]);
store = { trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [], comparison: c.dataset || [], pinnedIsins: store.pinnedIsins };
updateUI();
} catch (err) { console.error("Fetch error:", err); }
}
function updateUI() {
updateStats();
renderDashboardCharts();
renderCustomChart();
renderExchangeChart();
fillMetadataTable();
}
function updateStats() {
document.getElementById('statTrades').innerText = store.trades.length.toLocaleString();
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
let vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[4]) * parseFloat(r[5])), 0);
document.getElementById('statVolume').innerText = vol >= 1e6 ? `${(vol / 1e6).toFixed(2)}M` : `${(vol / 1e3).toFixed(1)}k`;
}
function showView(viewId) {
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
document.getElementById(`view-${viewId}`).classList.remove('hidden');
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
document.getElementById(`nav-${viewId}`).classList.add('active-nav');
window.activeView = viewId;
fetchData();
}
function renderDashboardCharts() {
// Price Trend
const trendCtx = document.getElementById('priceChart').getContext('2d');
const sorted = [...store.trades].sort((a, b) => new Date(a[3]) - new Date(b[3]));
const samples = sorted.slice(-100);
if (charts.price) charts.price.destroy();
charts.price = new Chart(trendCtx, {
type: 'line',
data: {
labels: samples.map(r => {
const d = new Date(r[3]);
return isNaN(d) ? '?' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}),
datasets: [{
label: 'Execution Price',
data: samples.map(r => parseFloat(r[4])),
borderColor: '#38bdf8',
borderWidth: 2,
fill: true,
backgroundColor: 'rgba(56, 189, 248, 0.05)',
tension: 0.4,
pointRadius: 0
}]
},
options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false } }, y: { grid: { color: 'rgba(255,255,255,0.05)' } } }, plugins: { legend: { display: false } } }
});
// Continent Pie
const contCtx = document.getElementById('continentChart').getContext('2d');
if (charts.continent) charts.continent.destroy();
charts.continent = new Chart(contCtx, {
type: 'doughnut',
data: {
labels: store.summary.map(r => r[0]),
datasets: [{
data: store.summary.map(r => r[2]),
backgroundColor: ['#38bdf8', '#fbbf24', '#10b981', '#f87171', '#818cf8', '#a78bfa'],
borderWidth: 0,
hoverOffset: 20
}]
},
options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 } } } } }
});
}
function renderCustomChart() {
const ctx = document.getElementById('customChart').getContext('2d');
const x = document.getElementById('axisX').value;
const y = document.getElementById('axisY').value;
// Simple Dynamic Aggregation
let grouped = {};
store.comparison.forEach(r => {
const label = x === 'time' ? new Date(r[1]).toLocaleDateString() : r[0]; // Simplified
if (!grouped[label]) grouped[label] = { val: 0, count: 0, prices: 0 };
grouped[label].val += r[2]; // Volume
grouped[label].count += r[3]; // Count
grouped[label].prices += r[2] / (r[3] || 1); // Mock avg price
});
const labels = Object.keys(grouped);
let finalData = labels.map(l => {
if (y === 'volume') return grouped[l].val;
if (y === 'count') return grouped[l].count;
return grouped[l].prices / (grouped[l].count || 1);
});
if (charts.custom) charts.custom.destroy();
charts.custom = new Chart(ctx, {
type: x === 'time' ? 'line' : 'bar',
data: {
labels,
datasets: [{
label: `${y} by ${x}`,
data: finalData,
backgroundColor: '#38bdf8',
borderColor: '#38bdf8',
borderRadius: 8
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
function renderExchangeChart() {
const ctx = document.getElementById('exchangeChart').getContext('2d');
const dataMap = {};
store.trades.forEach(r => {
dataMap[r[0]] = (dataMap[r[0]] || 0) + 1;
});
if (charts.exchange) charts.exchange.destroy();
charts.exchange = new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(dataMap),
datasets: [{
data: Object.values(dataMap),
backgroundColor: ['#38bdf8', '#fbbf24'],
borderRadius: 10
}]
},
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
});
}
function fillMetadataTable() {
const tbody = document.getElementById('metadataRows');
tbody.innerHTML = '';
store.metadata.forEach(r => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-white/5 transition cursor-default group';
tr.innerHTML = `
<td class="p-4 px-6 font-mono text-sky-400 font-bold">${r[0]}</td>
<td class="p-4 font-semibold text-slate-200">${r[1]}</td>
<td class="p-4"><span class="px-2 py-1 bg-sky-500/10 text-sky-400 rounded-lg text-xs font-bold ring-1 ring-sky-500/20">${r[2]}</span></td>
<td class="p-4 text-slate-400">${r[3]}</td>
<td class="p-4"><span class="text-slate-500 italic">${r[4]}</span></td>
<td class="p-4 text-center">
<button onclick="pinIsin('${r[0]}')" class="glass-btn p-2 rounded-lg text-lg opacity-0 group-hover:opacity-100">📌</button>
</td>
`;
tbody.appendChild(tr);
});
}
function pinIsin(isin) {
if (!store.pinnedIsins.includes(isin)) {
store.pinnedIsins.push(isin);
updatePinnedUI();
}
}
function updatePinnedUI() {
const container = document.getElementById('activeIsins');
container.innerHTML = '';
store.pinnedIsins.forEach(isin => {
const chip = document.createElement('div');
chip.className = 'glass p-1 px-3 text-xs text-sky-400 border-sky-500/50 flex items-center bg-sky-950/50 cursor-pointer hover:bg-sky-900 transition';
chip.innerHTML = `${isin} <span class="ml-2 text-slate-500" onclick="unpinIsin('${isin}')">×</span>`;
container.appendChild(chip);
});
}
function unpinIsin(isin) {
store.pinnedIsins = store.pinnedIsins.filter(i => i !== isin);
updatePinnedUI();
}
function filterMetadata() {
const q = document.getElementById('metadataSearch').value.toLowerCase();
const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => {
const text = r.innerText.toLowerCase();
r.style.display = text.includes(q) ? '' : 'none';
});
}
window.onload = () => { window.activeView = 'dashboard'; fetchData(); setInterval(fetchData, 30000); };
</script>
</body>
</html>