updated
All checks were successful
Deployment / deploy-docker (push) Successful in 17s

This commit is contained in:
Melchior Reimers
2026-01-25 17:36:29 +01:00
parent 4f4d734643
commit 33f5c90fce
8 changed files with 996 additions and 355 deletions

View File

@@ -251,6 +251,40 @@
<div class="h-80"><canvas id="continentChart"></canvas></div>
</div>
</div>
<!-- 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 -->
@@ -268,10 +302,6 @@
<option value="1">Today</option>
<option value="7">Last 7 Days</option>
<option value="30">Last 30 Days</option>
<option value="42">Last 42 Days</option>
<option value="69">Last 69 Days</option>
<option value="180">Last 6 Months (180d)</option>
<option value="365">Last Year (365d)</option>
<option value="ytd">Year to Date (YTD)</option>
<option value="year">Full Year 2026</option>
<option value="custom">Custom Range...</option>
@@ -534,6 +564,10 @@
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
renderDashboardCharts();
fillMetadataTable();
// Lade Statistiken neu wenn Dashboard aktualisiert wird
if (window.activeView === 'dashboard') {
loadStatistics();
}
}
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
@@ -589,36 +623,18 @@
if (y === 'all') {
// Dual axis for breakdown
// Volume Dataset
const volData = labels.map(l => {
const row = data.find(r => r[0] === l && r[1] === name);
return row ? row[3] : 0; // value_volume is index 3
});
datasets.push({
label: `${name} (Vol)`,
data: volData,
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
}),
backgroundColor: `hsla(${hue}, 75%, 50%, 0.7)`,
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
borderWidth: 2,
yAxisID: 'y',
type: 'bar'
});
// Add MA7 for Volume if enough data points
if (volData.length > 7) {
const ma7 = calculateMA(volData, 7);
datasets.push({
label: `${name} (Vol MA7)`,
data: ma7,
borderColor: `hsla(${hue}, 90%, 80%, 0.8)`,
borderWidth: 1.5,
borderDash: [5, 5],
pointRadius: 0,
yAxisID: 'y',
type: 'line',
tension: 0.4
});
}
// Count Dataset
datasets.push({
label: `${name} (Cnt)`,
@@ -886,22 +902,6 @@
updateUrlParams();
}
function calculateMA(data, period) {
let ma = [];
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
ma.push(null);
continue;
}
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - j] || 0;
}
ma.push(sum / period);
}
return ma;
}
function fillMetadataTable() {
const tbody = document.getElementById('metadataRows');
tbody.innerHTML = store.metadata.map(r => `
@@ -931,7 +931,355 @@
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
}
window.onload = async () => { await fetchData(); syncStateFromUrl(); setInterval(fetchData, 30000); };
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>