Files
trading-daemon/dashboard/public/index.html

1286 lines
60 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading Intelligence Hub</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;
}
.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;
}
.config-sidebar {
width: 340px;
border-right: 1px solid rgba(255, 255, 255, 0.05);
flex-shrink: 0;
}
.btn-primary {
background: #38bdf8;
color: #0b1120;
padding: 10px 20px;
border-radius: 10px;
font-weight: 700;
transition: all 0.2s;
box-shadow: 0 4px 14px 0 rgba(56, 189, 248, 0.3);
}
.btn-primary:hover {
background: #7dd3fc;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4);
}
.suggestion-box {
position: absolute;
z-index: 50;
width: 100%;
top: 100%;
left: 0;
background: #1e293b;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
}
.suggestion-item {
padding: 10px 16px;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.suggestion-item:hover {
background: rgba(56, 189, 248, 0.1);
color: #38bdf8;
}
.field-label {
display: block;
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.input-glass {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 10px;
border-radius: 10px;
outline: none;
transition: all 0.2s;
}
.input-glass:focus {
border-color: #38bdf8;
background: rgba(56, 189, 248, 0.05);
}
2026-01-24 07:44:35 +01:00
.report-step {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
2026-01-25 14:35:49 +01:00
padding: 1.5rem;
2026-01-24 07:44:35 +01:00
border-radius: 12px;
overflow: hidden;
2026-01-25 14:35:49 +01:00
max-height: 1000px;
margin-bottom: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.05);
2026-01-24 07:44:35 +01:00
}
.step-active {
background: rgba(56, 189, 248, 0.05);
border: 1px solid rgba(56, 189, 248, 0.2);
opacity: 1 !important;
2026-01-25 14:35:49 +01:00
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
2026-01-24 07:44:35 +01:00
}
.step-completed {
2026-01-25 14:35:49 +01:00
max-height: 80px;
opacity: 0.7;
2026-01-24 07:44:35 +01:00
cursor: pointer;
2026-01-25 14:35:49 +01:00
padding-top: 1rem;
padding-bottom: 1rem;
background: rgba(255, 255, 255, 0.02);
2026-01-24 07:44:35 +01:00
}
.step-completed:hover {
opacity: 1;
2026-01-25 14:35:49 +01:00
background: rgba(255, 255, 255, 0.04);
border-color: rgba(56, 189, 248, 0.3);
2026-01-24 07:44:35 +01:00
}
.step-completed select,
2026-01-25 14:35:49 +01:00
.step-completed input,
.step-completed .relative,
.step-completed .pt-8,
.step-completed .grid {
display: none !important;
2026-01-24 07:44:35 +01:00
}
.step-summary {
2026-01-25 14:35:49 +01:00
font-size: 0.85rem;
2026-01-24 07:44:35 +01:00
color: #38bdf8;
font-weight: 700;
2026-01-25 14:35:49 +01:00
margin-top: 0.5rem;
2026-01-24 07:44:35 +01:00
background: rgba(56, 189, 248, 0.1);
2026-01-25 14:35:49 +01:00
padding: 6px 12px;
border-radius: 8px;
display: block;
width: fit-content;
border: 1px solid rgba(56, 189, 248, 0.2);
2026-01-24 07:44:35 +01:00
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(56, 189, 248, 0.1);
border-top: 3px solid #38bdf8;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body class="flex min-h-screen overflow-hidden">
<!-- Sidebar -->
<aside class="w-20 lg:w-64 glass m-4 mr-0 flex flex-col hidden sm:flex">
<div class="p-6 hidden lg:block">
<h1 class="text-2xl font-bold bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text text-transparent">
Antigravity</h1>
</div>
<nav class="flex-1 px-4 space-y-2 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> <span
class="hidden lg:inline">Dashboard</span></a>
<a href="#" onclick="showView('analytics')" id="nav-analytics"
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">📈</span> <span
class="hidden lg:inline">Report Builder</span></a>
<a href="#" onclick="showView('metadata')" id="nav-metadata"
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">🏢</span> <span
class="hidden lg:inline">Companies</span></a>
</nav>
<div class="p-6 mt-auto hidden lg:block">
<div class="glass p-4 text-xs text-sky-400 border-sky-500/30">Status: <span
class="text-green-400 animate-pulse">Connected</span></div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0">
<header class="p-8 pb-4 flex justify-between items-center flex-shrink-0">
<div>
<h2 class="text-3xl font-bold" id="pageTitle">Market Overview</h2>
<div id="activeIsins" class="flex flex-wrap gap-2 mt-2"></div>
</div>
<button onclick="fetchData()" class="glass p-2 px-6 text-sky-400 font-bold hover:text-sky-200 transition">
REFRESH</button>
</header>
<!-- DASHBOARD VIEW -->
<div id="view-dashboard" class="view flex-1 p-8 pt-4 overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<div class="glass p-8 border-l-4 border-sky-400 glow">
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Volume (7d)</p>
<h3 id="statVolume" class="text-4xl font-bold">€0.0</h3>
</div>
<div class="glass p-8 border-l-4 border-amber-400">
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Total Trades</p>
<h3 id="statTrades" class="text-4xl font-bold">0</h3>
</div>
<div class="glass p-8 border-l-4 border-emerald-400">
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">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">
<div class="glass p-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Live Price Feed</h3>
<div class="h-80"><canvas id="priceChart"></canvas></div>
</div>
<div class="glass p-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Regional Distribution</h3>
<div class="h-80"><canvas id="continentChart"></canvas></div>
</div>
</div>
2026-01-25 17:36:29 +01:00
<!-- Neue Statistiken Sektion -->
<div class="mt-10 mb-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-slate-200">Erweiterte Statistiken</h2>
<select id="statisticsPeriod" class="input-glass px-4 py-2" onchange="loadStatistics()">
<option value="7">7 Tage</option>
<option value="30">30 Tage</option>
<option value="42">42 Tage</option>
<option value="69">69 Tage</option>
<option value="180">180 Tage</option>
<option value="365">365 Tage</option>
</select>
</div>
<!-- Moving Average Graph -->
<div class="glass p-8 mb-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Moving Average: Tradezahlen & Volumen je Exchange</h3>
<div class="h-96"><canvas id="movingAverageChart"></canvas></div>
</div>
<!-- Volumen-Änderungen -->
<div class="glass p-8 mb-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Tradingvolumen & Anzahl Änderungen</h3>
<div class="h-96"><canvas id="volumeChangesChart"></canvas></div>
</div>
<!-- Stock Trends -->
<div class="glass p-8">
<h3 class="text-lg font-bold mb-6 text-slate-300">Trendanalyse: Häufig gehandelte Aktien</h3>
<div class="h-96"><canvas id="stockTrendsChart"></canvas></div>
<div id="stockTrendsTable" class="mt-6 overflow-x-auto"></div>
</div>
</div>
</div>
<!-- REPORT BUILDER VIEW -->
<div id="view-analytics" class="view hidden flex-1 flex overflow-hidden">
<!-- Configuration Sidebar -->
<div class="config-sidebar p-8 space-y-6 overflow-y-auto bg-slate-900/30" id="reportConfig">
<h3 class="text-sky-400 font-bold text-xs uppercase tracking-tighter mb-4">Step-by-Step Configuration
</h3>
<!-- Step 1: Time Range -->
2026-01-24 07:44:35 +01:00
<div class="report-step step-active" id="step1">
<label class="field-label">1. Choose Analysis Period</label>
2026-01-24 07:44:35 +01:00
<select id="timeRangePreset" class="input-glass" onchange="updateUrlParams(); proceedToStep(2)">
<option value="" disabled selected>Select... </option>
<option value="1">Today</option>
<option value="7">Last 7 Days</option>
<option value="30">Last 30 Days</option>
<option value="ytd">Year to Date (YTD)</option>
<option value="year">Full Year 2026</option>
<option value="custom">Custom Range...</option>
</select>
<div id="customDates" class="hidden grid grid-cols-2 gap-2 mt-2">
2026-01-24 07:44:35 +01:00
<input type="date" id="dateFrom" class="input-glass text-xs p-2"
onchange="updateUrlParams(); checkCustomDates()">
<input type="date" id="dateTo" class="input-glass text-xs p-2"
onchange="updateUrlParams(); checkCustomDates()">
</div>
2026-01-24 07:44:35 +01:00
<div id="summary-step1" class="step-summary"></div>
</div>
<!-- Step 2: X-Axis -->
<div class="report-step hidden opacity-50" id="step2">
<label class="field-label">2. Primary Grouping (X-Axis)</label>
2026-01-24 07:44:35 +01:00
<select id="axisX" class="input-glass"
onchange="updateUrlParams(); proceedToStep(3); updateSubGroupOptions();">
<option value="" disabled selected>Select... </option>
<option value="day">Time (Daily)</option>
<option value="month">Time (Monthly)</option>
<option value="exchange">Exchange Origin</option>
<option value="continent">Region/Continent</option>
<option value="sector">Industry Sector</option>
<option value="isin">Specific ISIN</option>
</select>
2026-01-24 07:44:35 +01:00
<div id="summary-step2" class="step-summary"></div>
</div>
<!-- Step 3: Breakdown -->
<div class="report-step hidden opacity-50" id="step3">
<label class="field-label">3. Secondary Breakdown (Series)</label>
2026-01-24 07:44:35 +01:00
<select id="axisSub" class="input-glass" onchange="updateUrlParams(); proceedToStep(4)">
<option value="">None (Unified)</option>
2026-01-25 14:35:49 +01:00
<option value="isin">By Company / ISIN</option>
<option value="exchange">By Exchange</option>
<option value="continent">By Continent</option>
<option value="sector">By Sector</option>
2026-01-25 16:54:34 +01:00
<option value="exchange_continent">Exchange + Region</option>
<option value="exchange_sector">Exchange + Sector</option>
</select>
2026-01-24 07:44:35 +01:00
<div id="summary-step3" class="step-summary"></div>
</div>
<!-- Step 4: Y-Axis -->
<div class="report-step hidden opacity-50" id="step4">
<label class="field-label">4. Select Metric (Y-Axis)</label>
2026-01-24 07:44:35 +01:00
<select id="axisY" class="input-glass" onchange="updateUrlParams(); proceedToStep(5)">
<option value="" disabled selected>Select... </option>
<option value="volume">Trade Volume (€)</option>
<option value="count">Number of Trades</option>
<option value="avg_price">Average Performance</option>
2026-01-25 16:36:39 +01:00
<option value="all">Volume & Count (Dual Axis)</option>
</select>
2026-01-24 07:44:35 +01:00
<div id="summary-step4" class="step-summary"></div>
</div>
<!-- Step 5: Filters -->
<div class="report-step hidden opacity-50" id="step5">
<div class="relative">
<label class="field-label">5. Optional Company Filters</label>
<input type="text" id="isinSearch" autocomplete="off" placeholder="Search ISIN or Name..."
class="input-glass" onkeyup="handleSearch(event)">
<div id="suggestions" class="suggestion-box hidden"></div>
<div id="filterChips" class="flex flex-wrap gap-2 mt-4"></div>
</div>
<div class="pt-8 space-y-3">
2026-01-24 07:44:35 +01:00
<button id="btnGenerate" onclick="renderAnalyticsReport()"
class="btn-primary w-full shadow-sky-500/20">GENERATE REPORT</button>
<button onclick="resetReportConfig()"
class="w-full text-xs text-slate-500 hover:text-white transition">Reset
Configuration</button>
</div>
</div>
</div>
<div class="flex-1 p-8 overflow-hidden flex flex-col">
<div class="glass p-10 h-full flex flex-col shadow-2xl relative overflow-hidden">
<div class="flex justify-between items-center mb-10 z-10">
<h3 class="text-2xl font-bold" id="reportTitle">Report: Market Activity</h3>
<div class="flex glass p-1 rounded-lg">
<button class="p-2 px-4 text-xs font-bold rounded-md hover:bg-white/5"
onclick="setChartType('line')">Line</button>
<button class="p-2 px-4 text-xs font-bold rounded-md hover:bg-white/5"
onclick="setChartType('bar')">Bar</button>
</div>
</div>
2026-01-24 07:44:35 +01:00
<div class="flex-1 min-h-0 z-10 relative">
<div id="chartLoader"
class="hidden absolute inset-0 flex items-center justify-center bg-[#0b1120]/50 z-20">
<div class="spinner"></div>
</div>
<div id="chartError"
class="hidden absolute inset-0 flex items-center justify-center bg-red-900/20 z-20 p-10 text-center">
<div class="glass p-6 border-red-500/30">
<p class="text-red-400 font-bold mb-2">Analysis Failed</p>
<p id="errorMsg" class="text-xs text-slate-400"></p>
</div>
</div>
<canvas id="analyticsChart"></canvas>
</div>
<!-- Subtle watermark -->
<div class="absolute bottom-8 right-8 text-white/5 text-6xl font-black pointer-events-none">
ANALYTICS</div>
</div>
</div>
</div>
<!-- COMPANIES VIEW -->
<div id="view-metadata" class="view hidden flex-1 p-8 overflow-y-auto">
<div class="glass overflow-hidden">
<div class="p-8 border-b border-white/5 flex justify-between items-center">
<h3 class="text-xl font-bold">Metadata Repository</h3>
<input type="text" onkeyup="filterMetadata(this.value)"
placeholder="Search company, ISIN, sector..."
class="glass p-3 px-6 text-sm w-96 outline-none focus:border-sky-500">
</div>
<table class="w-full text-left">
<tbody id="metadataRows"></tbody>
</table>
</div>
</div>
</main>
<script>
const API = '/api';
let store = { trades: [], metadata: [], summary: [], pinnedIsins: [] };
let charts = {};
let currentChartType = 'bar';
let searchTimeout;
function showView(viewId) {
2026-01-24 07:44:35 +01:00
window.activeView = 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');
const titles = { 'dashboard': 'Market Overview', 'analytics': 'Custom Report Builder', 'metadata': 'Entity Metadata' };
document.getElementById('pageTitle').innerText = titles[viewId];
if (viewId === 'analytics') {
2026-01-24 07:44:35 +01:00
updateUrlParams();
} else {
fetchData();
}
}
function handlePresetChange() {
const v = document.getElementById('timeRangePreset').value;
document.getElementById('customDates').classList.toggle('hidden', v !== 'custom');
}
function updateSubGroupOptions() {
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub');
2026-01-25 16:54:34 +01:00
// Remove existing composite options first to avoid duplicates
// We need to reset the options or add them dynamically
// For simplicity, we'll just enable/disable standard ones and maybe append composite ones?
// Actually, let's just make sure they exist in the HTML (I will add them there)
// and toggling disabled state here.
Array.from(sub.options).forEach(opt => {
2026-01-25 16:54:34 +01:00
// Disable if same as X
opt.disabled = (opt.value === x);
2026-01-25 16:54:34 +01:00
// Composite options are special: only allow them if X is time-based (day/month)
if (['exchange_continent', 'exchange_sector'].includes(opt.value)) {
opt.disabled = !['day', 'month'].includes(x);
opt.hidden = !['day', 'month'].includes(x);
}
if (opt.value === x && sub.value === x) sub.value = '';
});
}
async function handleSearch(e) {
const q = e.target.value;
const sBox = document.getElementById('suggestions');
if (q.length < 2) { sBox.classList.add('hidden'); return; }
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const res = await fetch(`${API}/metadata/search?q=${q}`).then(r => r.json());
const data = res.dataset || [];
if (data.length > 0) {
sBox.innerHTML = data.map(item => `<div class="suggestion-item" onclick="addFilter('${item[0]}', '${item[1]}')">
<div class="font-bold">${item[0]}</div>
<div class="text-[10px] text-slate-500">${item[1]}</div>
</div>`).join('');
sBox.classList.remove('hidden');
} else {
sBox.classList.add('hidden');
}
}, 300);
}
function addFilter(isin, name) {
if (!store.pinnedIsins.find(p => p.isin === isin)) {
store.pinnedIsins.push({ isin, name });
updateFilterChips();
document.getElementById('isinSearch').value = '';
document.getElementById('suggestions').classList.add('hidden');
2026-01-24 07:44:35 +01:00
updateUrlParams();
if (window.activeView === 'analytics') proceedToStep(5);
}
}
function removeFilter(isin) {
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
updateFilterChips();
2026-01-24 07:44:35 +01:00
updateUrlParams();
}
function updateFilterChips() {
const container = document.getElementById('filterChips');
const pins = document.getElementById('activeIsins');
const html = store.pinnedIsins.map(p => `
<div class="glass p-1 px-3 text-[10px] font-bold text-sky-400 border-sky-400/30 flex items-center bg-sky-400/10">
${p.isin} <span class="ml-2 cursor-pointer text-slate-500 hover:text-white" onclick="removeFilter('${p.isin}')">×</span>
</div>
`).join('');
if (container) container.innerHTML = html;
if (pins) pins.innerHTML = html;
}
function getDates() {
const preset = document.getElementById('timeRangePreset').value;
const now = new Date();
let from, to = now.toISOString().split('T')[0];
if (preset === '1') from = to;
else if (preset === '7') from = new Date(now.setDate(now.getDate() - 7)).toISOString().split('T')[0];
else if (preset === '30') from = new Date(now.setDate(now.getDate() - 30)).toISOString().split('T')[0];
else if (preset === 'ytd') from = new Date(now.getFullYear(), 0, 1).toISOString().split('T')[0];
else if (preset === 'year') { from = '2026-01-01'; to = '2026-12-31'; }
else if (preset === 'custom') { from = document.getElementById('dateFrom').value; to = document.getElementById('dateTo').value; }
return { from, to };
}
async function fetchData() {
try {
const [t, m, s] = await Promise.all([
fetch(`${API}/trades?days=7`).then(r => r.json()),
fetch(`${API}/metadata`).then(r => r.json()),
fetch(`${API}/summary`).then(r => r.json())
]);
store = { ...store, trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [] };
updateDashboard();
} catch (err) { console.error(err); }
}
function updateDashboard() {
let vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[4]) * parseFloat(r[5] || 0)), 0);
document.getElementById('statVolume').innerText = vol >= 1e6 ? `€${(vol / 1e6).toFixed(1)}M` : `€${(vol / 1e3).toFixed(0)}k`;
document.getElementById('statTrades').innerText = store.trades.length.toLocaleString();
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
renderDashboardCharts();
fillMetadataTable();
2026-01-25 17:36:29 +01:00
// Lade Statistiken neu wenn Dashboard aktualisiert wird
if (window.activeView === 'dashboard') {
loadStatistics();
}
}
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
async function renderAnalyticsReport() {
2026-01-24 07:44:35 +01:00
const btn = document.getElementById('btnGenerate');
const loader = document.getElementById('chartLoader');
const errBox = document.getElementById('chartError');
const dates = getDates();
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub').value;
const y = document.getElementById('axisY').value;
const isins = store.pinnedIsins.map(p => p.isin).join(',');
if (!dates.from || !x || !y) return;
2026-01-24 07:44:35 +01:00
btn.innerText = 'GENERATING...';
btn.disabled = true;
loader.classList.remove('hidden');
errBox.classList.add('hidden');
let url = `${API}/analytics?metric=${y}&group_by=${x}`;
if (dates.from) url += `&date_from=${dates.from}`;
if (dates.to) url += `&date_to=${dates.to}`;
if (isins) url += `&isins=${isins}`;
try {
2026-01-25 14:35:49 +01:00
// If multiple ISINs are selected but no breakdown is chosen,
// we automatically breakdown by ISIN to give them different colors.
let effectiveSub = sub;
if (!sub && store.pinnedIsins.length > 1 && x !== 'isin') {
effectiveSub = 'isin';
url += `&sub_group_by=isin`;
} else if (sub) {
url += `&sub_group_by=${sub}`;
}
2026-01-24 07:44:35 +01:00
const res = await fetch(url).then(r => {
if (!r.ok) throw new Error(`Server Error: ${r.status}`);
return r.json();
});
const data = res.dataset || [];
const ctx = document.getElementById('analyticsChart').getContext('2d');
let labels = [...new Set(data.map(r => r[0]))];
if (x === 'day' || x === 'month') labels.sort((a, b) => new Date(a) - new Date(b));
let datasets = [];
2026-01-25 14:35:49 +01:00
if (effectiveSub) {
let seriesNames = [...new Set(data.map(r => r[1]))];
seriesNames.forEach((name, idx) => {
const hue = (idx * 137.5) % 360;
2026-01-25 16:36:39 +01:00
if (y === 'all') {
// Dual axis for breakdown
// Volume Dataset
datasets.push({
label: `${name} (Vol)`,
2026-01-25 17:36:29 +01:00
data: labels.map(l => {
const row = data.find(r => r[0] === l && r[1] === name);
return row ? row[3] : 0; // value_volume is index 3
}),
2026-01-25 16:36:39 +01:00
backgroundColor: `hsla(${hue}, 75%, 50%, 0.7)`,
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
borderWidth: 2,
yAxisID: 'y',
type: 'bar'
});
// Count Dataset
datasets.push({
label: `${name} (Cnt)`,
data: labels.map(l => {
const row = data.find(r => r[0] === l && r[1] === name);
return row ? row[2] : 0; // value_count is index 2
}),
backgroundColor: `hsla(${hue}, 75%, 30%, 0.7)`,
borderColor: `hsla(${hue}, 75%, 30%, 1)`,
borderWidth: 2,
yAxisID: 'y1',
type: 'line', // Always line for count to distinguish
pointStyle: 'rectRot'
});
} else {
// Standard single metric breakdown
datasets.push({
label: name,
data: labels.map(l => {
const row = data.find(r => r[0] === l && r[1] === name);
return row ? row[2] : 0;
}),
backgroundColor: `hsla(${hue}, 75%, 50%, 0.7)`,
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
borderWidth: 2,
fill: currentChartType === 'line',
tension: 0.3,
yAxisID: 'y'
});
}
});
} else {
if (y === 'all') {
// Standard dual axis without breakdown
datasets.push({
label: 'Volume (€)',
data: labels.map(l => {
const row = data.find(r => r[0] === l);
return row ? row[2] : 0; // value_volume
}),
backgroundColor: '#38bdf866',
borderColor: '#38bdf8',
borderWidth: 2,
yAxisID: 'y',
type: 'bar'
});
datasets.push({
label: 'Count',
data: labels.map(l => {
const row = data.find(r => r[0] === l);
return row ? row[1] : 0; // value_count
}),
backgroundColor: '#fbbf2466',
borderColor: '#fbbf24',
borderWidth: 2,
yAxisID: 'y1',
type: 'line'
});
} else {
datasets.push({
2026-01-25 16:36:39 +01:00
label: y.toUpperCase(),
data: labels.map(l => {
2026-01-25 16:36:39 +01:00
const row = data.find(r => r[0] === l);
return row ? row[1] : 0;
}),
2026-01-25 16:36:39 +01:00
backgroundColor: '#38bdf866',
borderColor: '#38bdf8',
borderWidth: 2,
2026-01-25 14:35:49 +01:00
fill: currentChartType === 'line',
2026-01-25 16:36:39 +01:00
tension: 0.3,
yAxisID: 'y'
});
2026-01-25 16:36:39 +01:00
}
}
if (charts.analytics) charts.analytics.destroy();
charts.analytics = new Chart(ctx, {
type: currentChartType,
data: { labels: labels.map(l => (x === 'day' || x === 'month') ? new Date(l).toLocaleDateString() : l), datasets },
options: {
responsive: true, maintainAspectRatio: false,
2026-01-24 07:44:35 +01:00
animation: { duration: 800, easing: 'easeOutQuart' },
2026-01-25 14:35:49 +01:00
scales: {
y: {
2026-01-25 16:36:39 +01:00
type: 'linear',
display: true,
position: 'left',
stacked: false,
2026-01-25 14:35:49 +01:00
grid: { color: 'rgba(255,255,255,0.05)' },
2026-01-25 16:36:39 +01:00
ticks: { color: '#64748b' },
title: { display: true, text: 'Volume (€)', color: '#38bdf8' }
},
y1: {
type: 'linear',
display: y === 'all',
position: 'right',
stacked: false,
grid: { drawOnChartArea: false }, // only want the grid lines for one axis to show up
ticks: { color: '#fbbf24' },
title: { display: true, text: 'Trade Count', color: '#fbbf24' }
2026-01-25 14:35:49 +01:00
},
x: {
stacked: false,
2026-01-25 14:35:49 +01:00
grid: { display: false },
ticks: { color: '#64748b' }
}
},
2026-01-24 07:44:35 +01:00
plugins: {
2026-01-25 14:35:49 +01:00
legend: {
display: true,
position: 'bottom',
labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 20 }
},
tooltip: {
backgroundColor: '#1e293b',
titleColor: '#38bdf8',
bodyColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.1)',
borderWidth: 1
}
2026-01-24 07:44:35 +01:00
}
}
});
2026-01-24 07:44:35 +01:00
document.getElementById('reportTitle').innerText = `Market Analytics: ${y.toUpperCase()} vs ${x.toUpperCase()}`;
} catch (err) {
console.error(err);
document.getElementById('errorMsg').innerText = err.message;
errBox.classList.remove('hidden');
} finally {
btn.innerText = 'GENERATE REPORT';
btn.disabled = false;
loader.classList.add('hidden');
}
}
function renderDashboardCharts() {
const trendCtx = document.getElementById('priceChart').getContext('2d');
const samples = [...store.trades].sort((a, b) => new Date(a[3]) - new Date(b[3])).slice(-100);
if (charts.price) charts.price.destroy();
charts.price = new Chart(trendCtx, {
type: 'line',
data: { labels: samples.map(r => new Date(r[3]).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })), datasets: [{ data: samples.map(r => r[4]), borderColor: '#38bdf8', borderWidth: 2, tension: 0.4, pointRadius: 0 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { grid: { color: 'rgba(255,255,255,0.05)' } } } }
});
const contCtx = document.getElementById('continentChart').getContext('2d');
if (charts.continent) charts.continent.destroy();
const labels = store.summary.map(r => r[0]);
2026-01-24 07:44:35 +01:00
const baseColors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#6366f1'];
charts.continent = new Chart(contCtx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: store.summary.map(r => r[2]),
backgroundColor: labels.map((l, i) => l.toLowerCase() === 'unknown' ? '#000000' : baseColors[i % baseColors.length]),
borderWidth: 0
}]
},
options: { responsive: true, maintainAspectRatio: false, cutout: '75%', plugins: { legend: { position: 'right', labels: { color: '#94a3b8' } } } }
});
}
function proceedToStep(n) {
2026-01-24 07:44:35 +01:00
document.querySelectorAll('.report-step').forEach((s, idx) => {
const stepNum = idx + 1;
2026-01-25 14:35:49 +01:00
// Active step is the one currently being configured
2026-01-24 07:44:35 +01:00
s.classList.toggle('step-active', stepNum === n);
2026-01-25 14:35:49 +01:00
// Completed steps are those before the current one, but NOT if they are active
s.classList.toggle('step-completed', stepNum < n && stepNum !== n);
// Show all steps but fade future ones slightly
s.classList.remove('hidden');
if (stepNum > n) {
s.classList.add('opacity-50');
} else {
s.classList.remove('opacity-50');
}
2026-01-24 07:44:35 +01:00
const summary = document.getElementById(`summary-step${stepNum}`);
if (summary) {
2026-01-25 14:35:49 +01:00
// Only show summary for non-active steps that have a selection
if (stepNum !== n) {
2026-01-24 07:44:35 +01:00
const input = s.querySelector('select, input');
let val = input ? (input.options && input.selectedIndex >= 0 ? input.options[input.selectedIndex].text : input.value) : '';
2026-01-25 14:35:49 +01:00
if (val && val !== 'Select...') {
summary.innerText = val;
summary.classList.remove('hidden');
} else {
summary.classList.add('hidden');
}
// Allow clicking headers of any step to go back/forward
2026-01-24 07:44:35 +01:00
s.onclick = () => proceedToStep(stepNum);
} else {
summary.classList.add('hidden');
s.onclick = null;
}
}
});
updateUrlParams();
}
function checkCustomDates() {
const from = document.getElementById('dateFrom').value;
const to = document.getElementById('dateTo').value;
if (from && to) proceedToStep(2);
}
2026-01-24 07:44:35 +01:00
function updateUrlParams() {
const params = new URLSearchParams(window.location.search);
const time = document.getElementById('timeRangePreset').value;
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub') ? document.getElementById('axisSub').value : '';
const y = document.getElementById('axisY').value;
const isins = store.pinnedIsins.map(p => p.isin).join(',');
if (time) params.set('time', time); else params.delete('time');
if (x) params.set('x', x); else params.delete('x');
if (sub) params.set('sub', sub); else params.delete('sub');
if (y) params.set('y', y); else params.delete('y');
if (isins) params.set('isins', isins); else params.delete('isins');
if (window.activeView) params.set('view', window.activeView);
// Important: Use hash only if it exists
const cleanUrl = window.location.pathname + '?' + params.toString();
window.history.replaceState({ selection: params.toString() }, '', cleanUrl);
}
function syncStateFromUrl() {
const params = new URLSearchParams(window.location.search);
const time = params.get('time');
const x = params.get('x');
const sub = params.get('sub');
const y = params.get('y');
const isins = params.get('isins');
const view = params.get('view');
if (view) showView(view);
if (time) { document.getElementById('timeRangePreset').value = time; proceedToStep(2); }
if (x) { document.getElementById('axisX').value = x; updateSubGroupOptions(); proceedToStep(3); }
if (sub) { document.getElementById('axisSub').value = sub; proceedToStep(4); }
if (y) { document.getElementById('axisY').value = y; proceedToStep(5); }
if (isins) {
setTimeout(() => {
isins.split(',').forEach(id => {
const ent = store.metadata.find(m => m[0] === id);
addFilter(id, ent ? ent[1] : id);
});
if (x && y && time) renderAnalyticsReport();
}, 800);
} else if (x && y && time) {
// Auto-generate report even without ISIN filters
setTimeout(() => renderAnalyticsReport(), 1000);
2026-01-24 07:44:35 +01:00
}
}
function resetReportConfig() {
2026-01-24 07:44:35 +01:00
document.querySelectorAll('.report-step').forEach((s, i) => { if (i > 0) s.classList.add('hidden', 'opacity-50'); else s.classList.add('step-active'); });
document.getElementById('timeRangePreset').value = '';
document.getElementById('axisX').value = '';
document.getElementById('axisSub').value = '';
document.getElementById('axisY').value = '';
store.pinnedIsins = [];
updateFilterChips();
2026-01-24 07:44:35 +01:00
updateUrlParams();
}
function fillMetadataTable() {
const tbody = document.getElementById('metadataRows');
tbody.innerHTML = store.metadata.map(r => `
<tr class="hover:bg-white/5 transition border-b border-white/5 cursor-pointer" onclick="deepLinkToAnalytics('${r[0]}', '${r[1]}')">
<td class="p-5 px-8 font-mono text-sky-400 font-bold">${r[0]}</td>
<td class="p-5 font-semibold text-slate-200">${r[1]}</td>
<td class="p-5 text-slate-500">${r[2]}</td>
<td class="p-5"><span class="bg-white/5 px-2 py-1 rounded text-[10px] text-slate-400 font-bold uppercase">${r[4] || 'UNKNOWN'}</span></td>
</tr>
`).join('');
}
function deepLinkToAnalytics(isin, name) {
resetReportConfig();
document.getElementById('timeRangePreset').value = '30';
document.getElementById('axisX').value = 'day';
document.getElementById('axisY').value = 'volume';
store.pinnedIsins = [{ isin, name }];
updateFilterChips();
showView('analytics');
2026-01-24 07:44:35 +01:00
for (let i = 1; i <= 5; i++) proceedToStep(i);
renderAnalyticsReport();
}
function filterMetadata(q) {
const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
}
2026-01-25 17:36:29 +01:00
async function loadStatistics() {
const days = document.getElementById('statisticsPeriod').value;
await Promise.all([
loadMovingAverage(days),
loadVolumeChanges(days),
loadStockTrends(days)
]);
}
async function loadMovingAverage(days) {
try {
const res = await fetch(`${API}/statistics/moving-average?days=${days}`).then(r => r.json());
const data = res.dataset || [];
const columns = res.columns || [];
if (!data.length) {
console.log('No moving average data available');
return;
}
const ctx = document.getElementById('movingAverageChart').getContext('2d');
if (charts.movingAverage) charts.movingAverage.destroy();
// Finde Spaltenindizes
const dateIdx = columns.findIndex(c => c.name === 'date' || c.name === 'timestamp');
const exchangeIdx = columns.findIndex(c => c.name === 'exchange');
const countIdx = columns.findIndex(c => c.name === 'trade_count');
const volumeIdx = columns.findIndex(c => c.name === 'volume');
const maCountIdx = columns.findIndex(c => c.name === 'ma_count');
const maVolumeIdx = columns.findIndex(c => c.name === 'ma_volume');
// Gruppiere nach Exchange
const exchanges = [...new Set(data.map(r => r[exchangeIdx]))];
const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
const datasets = [];
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6'];
// Trade Count Datasets
exchanges.forEach((exchange, idx) => {
datasets.push({
label: `${exchange} - Trade Count`,
data: dates.map(d => {
const row = data.find(r => r[dateIdx] === d && r[exchangeIdx] === exchange);
return row ? (row[countIdx] || 0) : 0;
}),
borderColor: colors[idx % colors.length],
backgroundColor: colors[idx % colors.length] + '33',
borderWidth: 2,
yAxisID: 'y',
tension: 0.3
});
});
// Moving Average Datasets
exchanges.forEach((exchange, idx) => {
datasets.push({
label: `${exchange} - MA Count`,
data: dates.map(d => {
const row = data.find(r => r[dateIdx] === d && r[exchangeIdx] === exchange);
return row ? (row[maCountIdx] || 0) : 0;
}),
borderColor: colors[idx % colors.length],
backgroundColor: 'transparent',
borderWidth: 2,
borderDash: [5, 5],
yAxisID: 'y',
tension: 0.3
});
});
// Volume Datasets
exchanges.forEach((exchange, idx) => {
datasets.push({
label: `${exchange} - Volume`,
data: dates.map(d => {
const row = data.find(r => r[dateIdx] === d && r[exchangeIdx] === exchange);
return row ? (row[volumeIdx] || 0) : 0;
}),
borderColor: colors[(idx + 2) % colors.length],
backgroundColor: colors[(idx + 2) % colors.length] + '33',
borderWidth: 2,
yAxisID: 'y1',
tension: 0.3
});
});
charts.movingAverage = new Chart(ctx, {
type: 'line',
data: {
labels: dates.map(d => new Date(d).toLocaleDateString()),
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: 'Trade Count', color: '#94a3b8' },
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#64748b' }
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: 'Volume (€)', color: '#94a3b8' },
grid: { drawOnChartArea: false },
ticks: { color: '#64748b' }
},
x: {
grid: { display: false },
ticks: { color: '#64748b' }
}
},
plugins: {
legend: {
display: true,
position: 'bottom',
labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 }
},
tooltip: {
backgroundColor: '#1e293b',
titleColor: '#38bdf8',
bodyColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.1)',
borderWidth: 1
}
}
}
});
} catch (err) {
console.error('Error loading moving average:', err);
}
}
async function loadVolumeChanges(days) {
try {
const res = await fetch(`${API}/statistics/volume-changes?days=${days}`).then(r => r.json());
const data = res.dataset || [];
const columns = res.columns || [];
if (!data.length) {
console.log('No volume changes data available');
return;
}
const ctx = document.getElementById('volumeChangesChart').getContext('2d');
if (charts.volumeChanges) charts.volumeChanges.destroy();
// Finde Spaltenindizes
const exchangeIdx = columns.findIndex(c => c.name === 'exchange');
const countChangeIdx = columns.findIndex(c => c.name === 'count_change_pct');
const volumeChangeIdx = columns.findIndex(c => c.name === 'volume_change_pct');
const trendIdx = columns.findIndex(c => c.name === 'trend');
const exchanges = data.map(r => r[exchangeIdx]);
const countChanges = data.map(r => r[countChangeIdx] || 0);
const volumeChanges = data.map(r => r[volumeChangeIdx] || 0);
charts.volumeChanges = new Chart(ctx, {
type: 'bar',
data: {
labels: exchanges,
datasets: [
{
label: 'Anzahl Änderung (%)',
data: countChanges,
backgroundColor: '#38bdf866',
borderColor: '#38bdf8',
borderWidth: 2,
yAxisID: 'y'
},
{
label: 'Volumen Änderung (%)',
data: volumeChanges,
backgroundColor: '#fbbf2466',
borderColor: '#fbbf24',
borderWidth: 2,
yAxisID: 'y'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
title: { display: true, text: 'Änderung (%)', color: '#94a3b8' },
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#64748b' }
},
x: {
grid: { display: false },
ticks: { color: '#64748b' }
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 }
},
tooltip: {
backgroundColor: '#1e293b',
titleColor: '#38bdf8',
bodyColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.1)',
borderWidth: 1,
callbacks: {
afterLabel: (context) => {
const idx = context.dataIndex;
const trend = data[idx][trendIdx]; // trend
return `Trend: ${trend || 'N/A'}`;
}
}
}
}
}
});
} catch (err) {
console.error('Error loading volume changes:', err);
}
}
async function loadStockTrends(days) {
try {
const res = await fetch(`${API}/statistics/stock-trends?days=${days}&limit=20`).then(r => r.json());
const data = res.dataset || [];
const columns = res.columns || [];
if (!data.length) {
console.log('No stock trends data available');
return;
}
const ctx = document.getElementById('stockTrendsChart').getContext('2d');
const tableContainer = document.getElementById('stockTrendsTable');
if (charts.stockTrends) charts.stockTrends.destroy();
// Finde Spaltenindizes
const isinIdx = columns.findIndex(c => c.name === 'isin');
const volumeIdx = columns.findIndex(c => c.name === 'volume');
const countIdx = columns.findIndex(c => c.name === 'trade_count');
const countChangeIdx = columns.findIndex(c => c.name === 'count_change_pct');
const volumeChangeIdx = columns.findIndex(c => c.name === 'volume_change_pct');
// Sortiere nach Volumen
const sorted = [...data].sort((a, b) => (b[volumeIdx] || 0) - (a[volumeIdx] || 0)).slice(0, 10);
const isins = sorted.map(r => r[isinIdx]);
const volumes = sorted.map(r => r[volumeIdx] || 0);
const countChanges = sorted.map(r => r[countChangeIdx] || 0);
const volumeChanges = sorted.map(r => r[volumeChangeIdx] || 0);
charts.stockTrends = new Chart(ctx, {
type: 'bar',
data: {
labels: isins.map(i => i.substring(0, 12) + '...'),
datasets: [
{
label: 'Volumen (€)',
data: volumes,
backgroundColor: '#10b98166',
borderColor: '#10b981',
borderWidth: 2,
yAxisID: 'y'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
title: { display: true, text: 'Volumen (€)', color: '#94a3b8' },
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#64748b' }
},
x: {
grid: { display: false },
ticks: { color: '#64748b', maxRotation: 45, minRotation: 45 }
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 }
},
tooltip: {
backgroundColor: '#1e293b',
titleColor: '#38bdf8',
bodyColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.1)',
borderWidth: 1
}
}
}
});
// Erstelle Tabelle
tableContainer.innerHTML = `
<table class="w-full text-left text-sm">
<thead>
<tr class="border-b border-white/10">
<th class="p-3 text-slate-400 font-bold">ISIN</th>
<th class="p-3 text-slate-400 font-bold">Trades</th>
<th class="p-3 text-slate-400 font-bold">Volumen (€)</th>
<th class="p-3 text-slate-400 font-bold">Anzahl Δ (%)</th>
<th class="p-3 text-slate-400 font-bold">Volumen Δ (%)</th>
</tr>
</thead>
<tbody>
${sorted.map(r => `
<tr class="border-b border-white/5 hover:bg-white/5">
<td class="p-3 font-mono text-sky-400">${r[isinIdx]}</td>
<td class="p-3 text-slate-300">${(r[countIdx] || 0).toLocaleString()}</td>
<td class="p-3 text-slate-300">€${((r[volumeIdx] || 0) / 1e6).toFixed(2)}M</td>
<td class="p-3 ${(r[countChangeIdx] || 0) >= 0 ? 'text-green-400' : 'text-red-400'}">${((r[countChangeIdx] || 0)).toFixed(2)}%</td>
<td class="p-3 ${(r[volumeChangeIdx] || 0) >= 0 ? 'text-green-400' : 'text-red-400'}">${((r[volumeChangeIdx] || 0)).toFixed(2)}%</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch (err) {
console.error('Error loading stock trends:', err);
}
}
window.onload = async () => {
await fetchData();
syncStateFromUrl();
setInterval(fetchData, 30000);
// Lade Statistiken beim Start
setTimeout(() => loadStatistics(), 1000);
};
</script>
</body>
</html>