886 lines
46 KiB
HTML
886 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<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>
|
|
<script src="/static/analytics-config.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; }
|
|
.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; color: #e2e8f0; }
|
|
.input-glass:focus { border-color: #38bdf8; background: rgba(56, 189, 248, 0.05); }
|
|
.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">
|
|
<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">Trading Hub</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('custom-analytics')" id="nav-custom-analytics" class="flex items-center p-3 rounded-xl transition">
|
|
<span class="mr-3">📈</span> <span class="hidden lg:inline">Custom Analytics</span>
|
|
</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
<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>
|
|
<button onclick="fetchData()" class="glass p-2 px-6 text-sky-400 font-bold hover:text-sky-200 transition">↻ REFRESH</button>
|
|
</header>
|
|
|
|
<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">
|
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2" id="statVolumeLabel">Volume</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" id="statTradesLabel">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="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>
|
|
|
|
<div class="mb-8">
|
|
<h3 class="text-lg font-bold mb-6 text-slate-300">Moving Average: Tradezahlen & Volumen je Exchange</h3>
|
|
<p class="text-sm text-slate-500 mb-4">Blau = Trades (7-Tage MA), Grün = Volumen (7-Tage MA)</p>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-sky-400">EIX</h4>
|
|
<div class="h-48"><canvas id="maChartEIX"></canvas></div>
|
|
</div>
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-rose-400">LS (Lang & Schwarz)</h4>
|
|
<div class="h-48"><canvas id="maChartLS"></canvas></div>
|
|
</div>
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-emerald-400">GETTEX</h4>
|
|
<div class="h-48"><canvas id="maChartGETTEX"></canvas></div>
|
|
</div>
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-amber-400">XETRA</h4>
|
|
<div class="h-48"><canvas id="maChartXETRA"></canvas></div>
|
|
</div>
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-violet-400">Frankfurt (FRA)</h4>
|
|
<div class="h-48"><canvas id="maChartFRA"></canvas></div>
|
|
</div>
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-orange-400">Stuttgart (STU)</h4>
|
|
<div class="h-48"><canvas id="maChartSTU"></canvas></div>
|
|
</div>
|
|
<div class="glass p-4">
|
|
<h4 class="text-md font-bold mb-3 text-pink-400">Quotrix</h4>
|
|
<div class="h-48"><canvas id="maChartQUOTRIX"></canvas></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<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>
|
|
|
|
<!-- CUSTOM ANALYTICS VIEW -->
|
|
<div id="view-custom-analytics" class="view hidden flex-1 p-8 pt-4 overflow-y-auto">
|
|
<div class="glass p-8 mb-6">
|
|
<h2 class="text-2xl font-bold text-slate-200 mb-6">Custom Analytics Graph</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div>
|
|
<label class="block text-sm font-bold text-slate-400 mb-2">X-Achse (Zeitraum)</label>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<input type="date" id="customDateFrom" class="input-glass" onchange="updateCustomGraph(); updateUrlParams()">
|
|
<input type="date" id="customDateTo" class="input-glass" onchange="updateCustomGraph(); updateUrlParams()">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-slate-400 mb-2">Y-Achse (Metrik)</label>
|
|
<select id="customYAxis" class="input-glass" onchange="updateCustomGraph(); updateUrlParams()">
|
|
<option value="volume">Volumen</option>
|
|
<option value="trade_count">Tradezahlen</option>
|
|
<option value="avg_price">Durchschnittspreis</option>
|
|
</select>
|
|
<p class="text-xs text-slate-500 mt-1" id="yAxisDescription"></p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-slate-400 mb-2">Gruppierung</label>
|
|
<select id="customGroupBy" class="input-glass" onchange="updateCustomGraph(); updateUrlParams()">
|
|
<option value="exchange">Exchange</option>
|
|
<option value="isin">ISIN</option>
|
|
<option value="date">Datum</option>
|
|
</select>
|
|
<p class="text-xs text-slate-500 mt-1" id="groupByDescription"></p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-slate-400 mb-2">Exchanges (optional, komma-separiert)</label>
|
|
<input type="text" id="customExchanges" class="input-glass" placeholder="z.B. EIX,LS,XETRA,FRA,GETTEX,STU,QUOTRIX" onchange="updateCustomGraph(); updateUrlParams()">
|
|
</div>
|
|
</div>
|
|
|
|
<button onclick="updateCustomGraph()" class="glass px-6 py-3 bg-sky-500 hover:bg-sky-600 text-white font-bold rounded-lg transition-colors cursor-pointer">Graph aktualisieren</button>
|
|
<button onclick="shareCustomGraph()" class="glass px-6 py-3 ml-2 text-sky-400 hover:text-sky-200 font-bold rounded-lg transition-colors cursor-pointer">🔗 Link teilen</button>
|
|
</div>
|
|
|
|
<div class="glass p-8">
|
|
<h3 class="text-lg font-bold mb-6 text-slate-300" id="customGraphTitle">Custom Analytics Graph</h3>
|
|
<div class="h-96"><canvas id="customAnalyticsChart"></canvas></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API = '/api';
|
|
let store = { trades: [], metadata: [], summary: [], totalTrades: 0 };
|
|
let charts = {};
|
|
let isRefreshing = false;
|
|
let refreshInterval = null;
|
|
|
|
async function fetchData(skipCharts = false) {
|
|
if (isRefreshing) return; // Verhindere parallele Aufrufe
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const days = parseInt(document.getElementById('statisticsPeriod')?.value || '7');
|
|
// Lade alle Trades (aggregiert) und Total Trades aus vorberechneten Daten
|
|
const [t, m, s, totalTrades] = await Promise.all([
|
|
fetch(`${API}/trades?days=${days}`).then(r => r.json()),
|
|
fetch(`${API}/metadata`).then(r => r.json()),
|
|
fetch(`${API}/summary?days=${days}`).then(r => r.json()),
|
|
fetch(`${API}/statistics/total-trades?days=${days}`).then(r => r.json())
|
|
]);
|
|
store = { ...store, trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [] };
|
|
store.totalTrades = totalTrades.total_trades || 0;
|
|
updateDashboard(days, skipCharts);
|
|
} catch (err) {
|
|
console.error('Error fetching data:', err);
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
function updateDashboard(days = 7, skipCharts = false) {
|
|
// Volumen aus dem gewählten Zeitraum (aggregiert)
|
|
let vol = 0;
|
|
if (store.trades && store.trades.length > 0) {
|
|
// Trades sind jetzt aggregiert: [date, exchange, trade_count, volume]
|
|
vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[3] || 0)), 0);
|
|
}
|
|
document.getElementById('statVolume').innerText = vol >= 1e6 ? `€${(vol / 1e6).toFixed(1)}M` : `€${(vol / 1e3).toFixed(0)}k`;
|
|
document.getElementById('statVolumeLabel').innerText = `Volume (${days}d)`;
|
|
|
|
// Total Trades aus vorberechneten Daten für den gewählten Zeitraum
|
|
document.getElementById('statTrades').innerText = (store.totalTrades || 0).toLocaleString();
|
|
document.getElementById('statTradesLabel').innerText = `Total Trades (${days}d)`;
|
|
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
|
|
|
|
// Lade Statistiken nur wenn nicht übersprungen (verhindert Graph-Zucken)
|
|
if (!skipCharts) {
|
|
loadStatistics();
|
|
}
|
|
}
|
|
|
|
function showView(viewId) {
|
|
if (!viewId) viewId = 'dashboard'; // Default zu dashboard
|
|
window.activeView = viewId;
|
|
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
|
|
const targetView = document.getElementById(`view-${viewId}`);
|
|
if (targetView) {
|
|
targetView.classList.remove('hidden');
|
|
}
|
|
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
|
|
const targetNav = document.getElementById(`nav-${viewId}`);
|
|
if (targetNav) {
|
|
targetNav.classList.add('active-nav');
|
|
}
|
|
|
|
const titles = { 'dashboard': 'Market Overview', 'custom-analytics': 'Custom Analytics' };
|
|
const pageTitle = document.getElementById('pageTitle');
|
|
if (pageTitle) {
|
|
pageTitle.innerText = titles[viewId] || 'Dashboard';
|
|
}
|
|
|
|
updateUrlParams();
|
|
}
|
|
|
|
async function loadStatistics(forceReload = false) {
|
|
const days = parseInt(document.getElementById('statisticsPeriod')?.value || '7');
|
|
|
|
// Beim ersten Laden oder bei expliziter Anforderung: Daten laden
|
|
if (forceReload || !store.trades || store.trades.length === 0) {
|
|
await fetchData(true); // skipCharts = true, damit Graphen nicht doppelt geladen werden
|
|
}
|
|
|
|
// Lade die Graphen
|
|
try {
|
|
await Promise.all([
|
|
loadMovingAverage(days),
|
|
loadVolumeChanges(days),
|
|
loadStockTrends(days)
|
|
]);
|
|
} catch (err) {
|
|
console.error('Error loading statistics:', err);
|
|
}
|
|
updateUrlParams();
|
|
}
|
|
|
|
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 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');
|
|
|
|
// Alle Exchanges mit Canvas-IDs definieren
|
|
const exchangeGroups = {
|
|
'EIX': { exchanges: ['EIX'], canvasId: 'maChartEIX' },
|
|
'LS': { exchanges: ['LS'], canvasId: 'maChartLS' },
|
|
'GETTEX': { exchanges: ['GETTEX'], canvasId: 'maChartGETTEX' },
|
|
'XETRA': { exchanges: ['XETRA'], canvasId: 'maChartXETRA' },
|
|
'FRA': { exchanges: ['FRA'], canvasId: 'maChartFRA' },
|
|
'STU': { exchanges: ['STU'], canvasId: 'maChartSTU' },
|
|
'QUOTRIX': { exchanges: ['QUOTRIX'], canvasId: 'maChartQUOTRIX' }
|
|
};
|
|
|
|
// Alle Daten nach Datum sortieren
|
|
const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
|
|
|
|
// Moving Average berechnen (7-Tage gleitender Durchschnitt)
|
|
const maWindow = 7;
|
|
|
|
const calculateMA = (dataArray, window) => {
|
|
return dataArray.map((val, idx) => {
|
|
if (idx < window - 1) return null;
|
|
const slice = dataArray.slice(idx - window + 1, idx + 1);
|
|
return slice.reduce((a, b) => a + b, 0) / window;
|
|
});
|
|
};
|
|
|
|
// Für jede Exchange-Gruppe einen separaten Chart erstellen
|
|
Object.entries(exchangeGroups).forEach(([groupName, config]) => {
|
|
const canvas = document.getElementById(config.canvasId);
|
|
if (!canvas) {
|
|
console.error(`Canvas element ${config.canvasId} not found`);
|
|
return;
|
|
}
|
|
|
|
// Zerstöre existierenden Chart
|
|
if (charts[config.canvasId]) charts[config.canvasId].destroy();
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Aggregiere Daten für diese Gruppe
|
|
const groupData = {};
|
|
dates.forEach(date => {
|
|
const dayRows = data.filter(r => {
|
|
const exchange = r[exchangeIdx];
|
|
return r[dateIdx] === date && config.exchanges.includes(exchange);
|
|
});
|
|
groupData[date] = {
|
|
tradeCount: dayRows.reduce((sum, r) => sum + (r[countIdx] || 0), 0),
|
|
volume: dayRows.reduce((sum, r) => sum + (r[volumeIdx] || 0), 0)
|
|
};
|
|
});
|
|
|
|
const tradeData = dates.map(d => groupData[d]?.tradeCount || 0);
|
|
const volumeData = dates.map(d => groupData[d]?.volume || 0);
|
|
const maTradeData = calculateMA(tradeData, maWindow);
|
|
const maVolumeData = calculateMA(volumeData, maWindow);
|
|
|
|
charts[config.canvasId] = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: dates.map(d => new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })),
|
|
datasets: [
|
|
{
|
|
label: 'Trades (MA)',
|
|
data: maTradeData,
|
|
borderColor: '#38bdf8',
|
|
backgroundColor: '#38bdf822',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
yAxisID: 'y',
|
|
tension: 0.4,
|
|
pointRadius: 0
|
|
},
|
|
{
|
|
label: 'Volumen (MA)',
|
|
data: maVolumeData,
|
|
borderColor: '#10b981',
|
|
backgroundColor: '#10b98122',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
yAxisID: 'y1',
|
|
tension: 0.4,
|
|
pointRadius: 0
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: 'index', intersect: false },
|
|
scales: {
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: { display: false },
|
|
grid: { color: 'rgba(255,255,255,0.05)' },
|
|
ticks: {
|
|
color: '#64748b',
|
|
font: { size: 10 },
|
|
callback: function(value) {
|
|
if (value >= 1e6) return (value / 1e6).toFixed(0) + 'M';
|
|
if (value >= 1e3) return (value / 1e3).toFixed(0) + 'k';
|
|
return value;
|
|
}
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
title: { display: false },
|
|
grid: { drawOnChartArea: false },
|
|
ticks: {
|
|
color: '#64748b',
|
|
font: { size: 10 },
|
|
callback: function(value) {
|
|
if (value >= 1e6) return '€' + (value / 1e6).toFixed(0) + 'M';
|
|
if (value >= 1e3) return '€' + (value / 1e3).toFixed(0) + 'k';
|
|
return '€' + value;
|
|
}
|
|
}
|
|
},
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: {
|
|
color: '#64748b',
|
|
maxRotation: 0,
|
|
font: { size: 9 },
|
|
maxTicksLimit: 8
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: { color: '#94a3b8', boxWidth: 10, usePointStyle: true, padding: 8, font: { size: 10 } }
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) label += ': ';
|
|
if (context.parsed.y !== null) {
|
|
if (context.dataset.yAxisID === 'y1') {
|
|
label += '€' + context.parsed.y.toLocaleString();
|
|
} else {
|
|
label += context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
} 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 canvas = document.getElementById('volumeChangesChart');
|
|
if (!canvas) {
|
|
console.error('Canvas element volumeChangesChart not found');
|
|
return;
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
if (charts.volumeChanges) charts.volumeChanges.destroy();
|
|
|
|
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: {
|
|
callbacks: {
|
|
afterLabel: (context) => {
|
|
const idx = context.dataIndex;
|
|
const trend = data[idx][trendIdx];
|
|
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 canvas = document.getElementById('stockTrendsChart');
|
|
if (!canvas) {
|
|
console.error('Canvas element stockTrendsChart not found');
|
|
return;
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const tableContainer = document.getElementById('stockTrendsTable');
|
|
|
|
if (charts.stockTrends) charts.stockTrends.destroy();
|
|
|
|
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');
|
|
|
|
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);
|
|
|
|
charts.stockTrends = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: isins.map(i => i ? i.substring(0, 12) + '...' : 'N/A'),
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
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] || 'N/A'}</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);
|
|
}
|
|
}
|
|
|
|
// Custom Analytics Funktionen
|
|
async function updateCustomGraph() {
|
|
const dateFrom = document.getElementById('customDateFrom').value;
|
|
const dateTo = document.getElementById('customDateTo').value;
|
|
const yAxis = document.getElementById('customYAxis').value;
|
|
const groupBy = document.getElementById('customGroupBy').value;
|
|
const exchanges = document.getElementById('customExchanges').value;
|
|
|
|
if (!dateFrom || !dateTo) {
|
|
console.log('Bitte wählen Sie Start- und Enddatum');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let url = `${API}/custom-analytics?date_from=${dateFrom}&date_to=${dateTo}&x_axis=date&y_axis=${yAxis}&group_by=${groupBy}`;
|
|
if (exchanges) {
|
|
url += `&exchanges=${encodeURIComponent(exchanges)}`;
|
|
}
|
|
|
|
const res = await fetch(url).then(r => r.json());
|
|
const data = res.dataset || [];
|
|
const columns = res.columns || [];
|
|
|
|
if (!data.length) {
|
|
console.log('Keine Daten für den gewählten Zeitraum');
|
|
return;
|
|
}
|
|
|
|
const ctx = document.getElementById('customAnalyticsChart').getContext('2d');
|
|
if (charts.customAnalytics) charts.customAnalytics.destroy();
|
|
|
|
const xIdx = columns.findIndex(c => c.name === 'x_value');
|
|
const groupIdx = columns.findIndex(c => c.name === 'group_value');
|
|
const yIdx = columns.findIndex(c => c.name === 'y_value');
|
|
|
|
const groups = [...new Set(data.map(r => r[groupIdx]).filter(g => g && g !== '' && g !== 'NONE'))];
|
|
const dates = [...new Set(data.map(r => r[xIdx]))].sort();
|
|
|
|
if (groups.length === 0) {
|
|
console.log('No valid groups found in data');
|
|
return;
|
|
}
|
|
|
|
// Erweiterte Farben für mehr Exchanges (EIX, LS, XETRA, FRA, GETTEX, STU, QUOTRIX)
|
|
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#14b8a6', '#84cc16', '#a855f7'];
|
|
const datasets = groups.map((group, idx) => ({
|
|
label: group || 'Unknown',
|
|
data: dates.map(d => {
|
|
const row = data.find(r => r[xIdx] === d && r[groupIdx] === group);
|
|
return row ? (row[yIdx] || 0) : 0;
|
|
}),
|
|
borderColor: colors[idx % colors.length],
|
|
backgroundColor: colors[idx % colors.length] + '33',
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
fill: false
|
|
}));
|
|
|
|
// Nutze Config für Y-Achsen-Label (falls verfügbar)
|
|
let yAxisLabel = 'Wert';
|
|
if (typeof ANALYTICS_CONFIG !== 'undefined' && ANALYTICS_CONFIG.yAxis && ANALYTICS_CONFIG.yAxis[yAxis]) {
|
|
const config = ANALYTICS_CONFIG.yAxis[yAxis];
|
|
yAxisLabel = `${config.label}${config.unit ? ' (' + config.unit + ')' : ''}`;
|
|
} else {
|
|
// Fallback
|
|
const labels = {
|
|
'volume': 'Volumen (€)',
|
|
'trade_count': 'Anzahl Trades',
|
|
'avg_price': 'Durchschnittspreis (€)'
|
|
};
|
|
yAxisLabel = labels[yAxis] || 'Wert';
|
|
}
|
|
|
|
charts.customAnalytics = 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,
|
|
title: { display: true, text: yAxisLabel, color: '#94a3b8' },
|
|
grid: { color: 'rgba(255,255,255,0.05)' },
|
|
ticks: { color: '#64748b' }
|
|
},
|
|
x: {
|
|
title: { display: true, text: 'Datum', color: '#94a3b8' },
|
|
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
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('customGraphTitle').innerText =
|
|
`${yAxisLabel} nach ${groupBy === 'exchange' ? 'Exchange' : groupBy === 'isin' ? 'ISIN' : 'Datum'} (${dateFrom} bis ${dateTo})`;
|
|
} catch (err) {
|
|
console.error('Error loading custom analytics:', err);
|
|
}
|
|
}
|
|
|
|
function shareCustomGraph() {
|
|
const params = new URLSearchParams();
|
|
params.set('view', 'custom-analytics');
|
|
params.set('date_from', document.getElementById('customDateFrom').value);
|
|
params.set('date_to', document.getElementById('customDateTo').value);
|
|
params.set('y_axis', document.getElementById('customYAxis').value);
|
|
params.set('group_by', document.getElementById('customGroupBy').value);
|
|
const exchanges = document.getElementById('customExchanges').value;
|
|
if (exchanges) params.set('exchanges', exchanges);
|
|
|
|
const url = window.location.origin + window.location.pathname + '?' + params.toString();
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
alert('Link in Zwischenablage kopiert!');
|
|
}).catch(() => {
|
|
prompt('Link zum Teilen:', url);
|
|
});
|
|
}
|
|
|
|
function updateUrlParams() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const view = window.activeView;
|
|
if (view) params.set('view', view);
|
|
|
|
if (view === 'custom-analytics') {
|
|
params.set('date_from', document.getElementById('customDateFrom').value || '');
|
|
params.set('date_to', document.getElementById('customDateTo').value || '');
|
|
params.set('y_axis', document.getElementById('customYAxis').value || '');
|
|
params.set('group_by', document.getElementById('customGroupBy').value || '');
|
|
const exchanges = document.getElementById('customExchanges').value;
|
|
if (exchanges) params.set('exchanges', exchanges);
|
|
else params.delete('exchanges');
|
|
} else if (view === 'dashboard') {
|
|
const period = document.getElementById('statisticsPeriod')?.value;
|
|
if (period) params.set('period', period);
|
|
}
|
|
|
|
window.history.replaceState({}, '', window.location.pathname + '?' + params.toString());
|
|
}
|
|
|
|
function loadFromUrlParams() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const view = params.get('view');
|
|
if (view) {
|
|
showView(view);
|
|
|
|
if (view === 'custom-analytics') {
|
|
const dateFrom = params.get('date_from');
|
|
const dateTo = params.get('date_to');
|
|
const yAxis = params.get('y_axis');
|
|
const groupBy = params.get('group_by');
|
|
const exchanges = params.get('exchanges');
|
|
|
|
if (dateFrom) document.getElementById('customDateFrom').value = dateFrom;
|
|
if (dateTo) document.getElementById('customDateTo').value = dateTo;
|
|
if (yAxis) document.getElementById('customYAxis').value = yAxis;
|
|
if (groupBy) document.getElementById('customGroupBy').value = groupBy;
|
|
if (exchanges) document.getElementById('customExchanges').value = exchanges;
|
|
|
|
if (dateFrom && dateTo) {
|
|
setTimeout(() => updateCustomGraph(), 500);
|
|
}
|
|
} else if (view === 'dashboard') {
|
|
const period = params.get('period');
|
|
if (period) {
|
|
document.getElementById('statisticsPeriod').value = period;
|
|
setTimeout(() => loadStatistics(), 500);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialisiere Custom Analytics Dropdowns aus Config (falls verfügbar)
|
|
function initCustomAnalyticsDropdowns() {
|
|
if (typeof ANALYTICS_CONFIG !== 'undefined') {
|
|
// Y-Achse Optionen
|
|
const yAxisSelect = document.getElementById('customYAxis');
|
|
if (yAxisSelect && ANALYTICS_CONFIG.yAxis) {
|
|
yAxisSelect.innerHTML = '';
|
|
Object.entries(ANALYTICS_CONFIG.yAxis).forEach(([key, config]) => {
|
|
const option = document.createElement('option');
|
|
option.value = key;
|
|
option.textContent = config.label;
|
|
yAxisSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// GroupBy Optionen
|
|
const groupBySelect = document.getElementById('customGroupBy');
|
|
if (groupBySelect && ANALYTICS_CONFIG.groupBy) {
|
|
groupBySelect.innerHTML = '';
|
|
Object.entries(ANALYTICS_CONFIG.groupBy).forEach(([key, config]) => {
|
|
const option = document.createElement('option');
|
|
option.value = key;
|
|
option.textContent = config.label;
|
|
groupBySelect.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
window.onload = async () => {
|
|
// Initialisiere Dropdowns aus Config
|
|
initCustomAnalyticsDropdowns();
|
|
|
|
// Setze Standard-Datum (letzte 30 Tage)
|
|
const today = new Date();
|
|
const thirtyDaysAgo = new Date(today);
|
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
|
if (!document.getElementById('customDateTo').value) {
|
|
document.getElementById('customDateTo').value = today.toISOString().split('T')[0];
|
|
}
|
|
if (!document.getElementById('customDateFrom').value) {
|
|
document.getElementById('customDateFrom').value = thirtyDaysAgo.toISOString().split('T')[0];
|
|
}
|
|
|
|
// Initialisiere Dashboard-View
|
|
showView('dashboard');
|
|
|
|
await fetchData();
|
|
loadFromUrlParams();
|
|
|
|
// Automatisches Refresh: Aktualisiere nur Daten, nicht die Graphen (verhindert Zucken)
|
|
refreshInterval = setInterval(() => {
|
|
fetchData(true); // skipCharts = true
|
|
}, 30000);
|
|
|
|
// Lade Statistiken nach kurzer Verzögerung (forceReload = true beim ersten Laden)
|
|
setTimeout(() => loadStatistics(true), 1000);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|