This commit is contained in:
86
dashboard/public/analytics-config.js
Normal file
86
dashboard/public/analytics-config.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Analytics Parameter Konfiguration
|
||||||
|
// Diese Datei definiert alle verfügbaren Parameter für Custom Analytics
|
||||||
|
// Neue Parameter können hier einfach hinzugefügt werden
|
||||||
|
|
||||||
|
const ANALYTICS_CONFIG = {
|
||||||
|
// X-Achse Optionen
|
||||||
|
xAxis: {
|
||||||
|
'date': {
|
||||||
|
label: 'Datum',
|
||||||
|
type: 'date',
|
||||||
|
description: 'Zeitraum auf der X-Achse'
|
||||||
|
},
|
||||||
|
'exchange': {
|
||||||
|
label: 'Exchange',
|
||||||
|
type: 'string',
|
||||||
|
description: 'Exchange auf der X-Achse'
|
||||||
|
},
|
||||||
|
'isin': {
|
||||||
|
label: 'ISIN',
|
||||||
|
type: 'string',
|
||||||
|
description: 'ISIN auf der X-Achse'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Y-Achse Optionen (Metriken)
|
||||||
|
yAxis: {
|
||||||
|
'volume': {
|
||||||
|
label: 'Volumen',
|
||||||
|
unit: '€',
|
||||||
|
description: 'Handelsvolumen in Euro'
|
||||||
|
},
|
||||||
|
'trade_count': {
|
||||||
|
label: 'Tradezahlen',
|
||||||
|
unit: '',
|
||||||
|
description: 'Anzahl der Trades'
|
||||||
|
},
|
||||||
|
'avg_price': {
|
||||||
|
label: 'Durchschnittspreis',
|
||||||
|
unit: '€',
|
||||||
|
description: 'Durchschnittlicher Handelspreis'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Gruppierungs-Optionen
|
||||||
|
groupBy: {
|
||||||
|
'exchange': {
|
||||||
|
label: 'Exchange',
|
||||||
|
description: 'Gruppierung nach Exchange'
|
||||||
|
},
|
||||||
|
'isin': {
|
||||||
|
label: 'ISIN',
|
||||||
|
description: 'Gruppierung nach ISIN'
|
||||||
|
},
|
||||||
|
'date': {
|
||||||
|
label: 'Datum',
|
||||||
|
description: 'Gruppierung nach Datum'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Filter-Optionen
|
||||||
|
filters: {
|
||||||
|
'exchanges': {
|
||||||
|
label: 'Exchanges',
|
||||||
|
type: 'multiselect',
|
||||||
|
description: 'Filter nach Exchanges (komma-separiert)'
|
||||||
|
},
|
||||||
|
'isins': {
|
||||||
|
label: 'ISINs',
|
||||||
|
type: 'multiselect',
|
||||||
|
description: 'Filter nach ISINs (komma-separiert)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hilfsfunktion zum Abrufen von Konfigurationswerten
|
||||||
|
function getConfig(category, key) {
|
||||||
|
return ANALYTICS_CONFIG[category]?.[key] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Hinzufügen neuer Parameter (für zukünftige Erweiterungen)
|
||||||
|
function addConfigParameter(category, key, config) {
|
||||||
|
if (!ANALYTICS_CONFIG[category]) {
|
||||||
|
ANALYTICS_CONFIG[category] = {};
|
||||||
|
}
|
||||||
|
ANALYTICS_CONFIG[category][key] = config;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Trading Intelligence Hub</title>
|
<title>Trading Intelligence Hub</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></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">
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Outfit', sans-serif; background-color: #0b1120; color: #e2e8f0; }
|
body { font-family: 'Outfit', sans-serif; background-color: #0b1120; color: #e2e8f0; }
|
||||||
@@ -26,6 +27,9 @@
|
|||||||
<a href="#" onclick="showView('dashboard')" id="nav-dashboard" class="flex items-center p-3 rounded-xl transition active-nav">
|
<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>
|
<span class="mr-3">📊</span> <span class="hidden lg:inline">Dashboard</span>
|
||||||
</a>
|
</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>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -40,11 +44,11 @@
|
|||||||
<div id="view-dashboard" class="view flex-1 p-8 pt-4 overflow-y-auto">
|
<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="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||||
<div class="glass p-8 border-l-4 border-sky-400">
|
<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">Volume (7d)</p>
|
<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>
|
<h3 id="statVolume" class="text-4xl font-bold">€0.0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass p-8 border-l-4 border-amber-400">
|
<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>
|
<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>
|
<h3 id="statTrades" class="text-4xl font-bold">0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass p-8 border-l-4 border-emerald-400">
|
<div class="glass p-8 border-l-4 border-emerald-400">
|
||||||
@@ -83,6 +87,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</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" onchange="updateCustomGraph(); updateUrlParams()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="updateCustomGraph()" class="btn-primary px-6 py-2">Graph aktualisieren</button>
|
||||||
|
<button onclick="shareCustomGraph()" class="glass px-6 py-2 ml-2 text-sky-400 hover:text-sky-200">🔗 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>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -92,32 +143,35 @@
|
|||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
|
const days = parseInt(document.getElementById('statisticsPeriod')?.value || '7');
|
||||||
// Lade alle Trades (aggregiert) und Total Trades aus vorberechneten Daten
|
// Lade alle Trades (aggregiert) und Total Trades aus vorberechneten Daten
|
||||||
const [t, m, s, totalTrades] = await Promise.all([
|
const [t, m, s, totalTrades] = await Promise.all([
|
||||||
fetch(`${API}/trades?days=7`).then(r => r.json()),
|
fetch(`${API}/trades?days=${days}`).then(r => r.json()),
|
||||||
fetch(`${API}/metadata`).then(r => r.json()),
|
fetch(`${API}/metadata`).then(r => r.json()),
|
||||||
fetch(`${API}/summary`).then(r => r.json()),
|
fetch(`${API}/summary?days=${days}`).then(r => r.json()),
|
||||||
fetch(`${API}/statistics/total-trades`).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 = { ...store, trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [] };
|
||||||
store.totalTrades = totalTrades.total_trades || 0;
|
store.totalTrades = totalTrades.total_trades || 0;
|
||||||
updateDashboard();
|
updateDashboard(days);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching data:', err);
|
console.error('Error fetching data:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDashboard() {
|
function updateDashboard(days = 7) {
|
||||||
// Volumen aus den letzten 7 Tagen (aggregiert)
|
// Volumen aus dem gewählten Zeitraum (aggregiert)
|
||||||
let vol = 0;
|
let vol = 0;
|
||||||
if (store.trades && store.trades.length > 0) {
|
if (store.trades && store.trades.length > 0) {
|
||||||
// Trades sind jetzt aggregiert: [date, exchange, trade_count, volume]
|
// Trades sind jetzt aggregiert: [date, exchange, trade_count, volume]
|
||||||
vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[3] || 0)), 0);
|
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('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 (ALLE Trades)
|
// Total Trades aus vorberechneten Daten für den gewählten Zeitraum
|
||||||
document.getElementById('statTrades').innerText = (store.totalTrades || 0).toLocaleString();
|
document.getElementById('statTrades').innerText = (store.totalTrades || 0).toLocaleString();
|
||||||
|
document.getElementById('statTradesLabel').innerText = `Total Trades (${days}d)`;
|
||||||
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
|
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
|
||||||
|
|
||||||
// Lade Statistiken
|
// Lade Statistiken
|
||||||
@@ -130,10 +184,18 @@
|
|||||||
document.getElementById(`view-${viewId}`).classList.remove('hidden');
|
document.getElementById(`view-${viewId}`).classList.remove('hidden');
|
||||||
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
|
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
|
||||||
document.getElementById(`nav-${viewId}`).classList.add('active-nav');
|
document.getElementById(`nav-${viewId}`).classList.add('active-nav');
|
||||||
|
|
||||||
|
const titles = { 'dashboard': 'Market Overview', 'custom-analytics': 'Custom Analytics' };
|
||||||
|
document.getElementById('pageTitle').innerText = titles[viewId] || 'Dashboard';
|
||||||
|
|
||||||
|
updateUrlParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStatistics() {
|
async function loadStatistics() {
|
||||||
const days = document.getElementById('statisticsPeriod').value;
|
const days = parseInt(document.getElementById('statisticsPeriod')?.value || '7');
|
||||||
|
// Aktualisiere auch die Statistik-Karten
|
||||||
|
await fetchData();
|
||||||
|
// Lade dann die Graphen
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadMovingAverage(days),
|
loadMovingAverage(days),
|
||||||
loadVolumeChanges(days),
|
loadVolumeChanges(days),
|
||||||
@@ -436,8 +498,238 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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]))];
|
||||||
|
const dates = [...new Set(data.map(r => r[xIdx]))].sort();
|
||||||
|
|
||||||
|
const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899'];
|
||||||
|
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 () => {
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
await fetchData();
|
await fetchData();
|
||||||
|
loadFromUrlParams();
|
||||||
setInterval(fetchData, 30000);
|
setInterval(fetchData, 30000);
|
||||||
setTimeout(() => loadStatistics(), 1000);
|
setTimeout(() => loadStatistics(), 1000);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -94,17 +94,28 @@ async def get_metadata():
|
|||||||
return format_questdb_response(data)
|
return format_questdb_response(data)
|
||||||
|
|
||||||
@app.get("/api/summary")
|
@app.get("/api/summary")
|
||||||
async def get_summary():
|
async def get_summary(days: int = None):
|
||||||
"""
|
"""
|
||||||
Gibt Zusammenfassung zurück. Nutzt analytics_daily_summary für total_trades (alle Trades).
|
Gibt Zusammenfassung zurück. Nutzt analytics_daily_summary für total_trades.
|
||||||
"""
|
Optional: days Parameter für Zeitraum-basierte Zusammenfassung.
|
||||||
# Hole Gesamtzahl aller Trades aus analytics_daily_summary
|
|
||||||
query = """
|
|
||||||
select
|
|
||||||
sum(total_trades) as total_trades,
|
|
||||||
sum(total_volume) as total_volume
|
|
||||||
from analytics_daily_summary
|
|
||||||
"""
|
"""
|
||||||
|
if days:
|
||||||
|
# Zeitraum-basierte Zusammenfassung
|
||||||
|
query = f"""
|
||||||
|
select
|
||||||
|
sum(total_trades) as total_trades,
|
||||||
|
sum(total_volume) as total_volume
|
||||||
|
from analytics_daily_summary
|
||||||
|
where timestamp >= dateadd('d', -{days}, now())
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
# Gesamtzahl aller Trades
|
||||||
|
query = """
|
||||||
|
select
|
||||||
|
sum(total_trades) as total_trades,
|
||||||
|
sum(total_volume) as total_volume
|
||||||
|
from analytics_daily_summary
|
||||||
|
"""
|
||||||
|
|
||||||
data = query_questdb(query)
|
data = query_questdb(query)
|
||||||
if data and data.get('dataset') and data['dataset']:
|
if data and data.get('dataset') and data['dataset']:
|
||||||
@@ -135,15 +146,92 @@ async def get_summary():
|
|||||||
return format_questdb_response(data)
|
return format_questdb_response(data)
|
||||||
|
|
||||||
@app.get("/api/statistics/total-trades")
|
@app.get("/api/statistics/total-trades")
|
||||||
async def get_total_trades():
|
async def get_total_trades(days: int = None):
|
||||||
"""Gibt Gesamtzahl aller Trades zurück (aus analytics_daily_summary)"""
|
"""Gibt Gesamtzahl aller Trades zurück (aus analytics_daily_summary). Optional: days Parameter für Zeitraum."""
|
||||||
query = "select sum(total_trades) as total from analytics_daily_summary"
|
if days:
|
||||||
|
query = f"select sum(total_trades) as total from analytics_daily_summary where timestamp >= dateadd('d', -{days}, now())"
|
||||||
|
else:
|
||||||
|
query = "select sum(total_trades) as total from analytics_daily_summary"
|
||||||
data = query_questdb(query)
|
data = query_questdb(query)
|
||||||
if data and data.get('dataset') and data['dataset']:
|
if data and data.get('dataset') and data['dataset']:
|
||||||
total = data['dataset'][0][0] if data['dataset'][0][0] else 0
|
total = data['dataset'][0][0] if data['dataset'][0][0] else 0
|
||||||
return {'total_trades': total}
|
return {'total_trades': total}
|
||||||
return {'total_trades': 0}
|
return {'total_trades': 0}
|
||||||
|
|
||||||
|
@app.get("/api/custom-analytics")
|
||||||
|
async def get_custom_analytics(
|
||||||
|
date_from: str,
|
||||||
|
date_to: str,
|
||||||
|
x_axis: str = "date",
|
||||||
|
y_axis: str = "volume",
|
||||||
|
group_by: str = "exchange",
|
||||||
|
exchanges: str = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Flexibler Analytics-Endpunkt für custom Graphen.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- date_from: Startdatum (YYYY-MM-DD)
|
||||||
|
- date_to: Enddatum (YYYY-MM-DD)
|
||||||
|
- x_axis: X-Achse (date, exchange, isin)
|
||||||
|
- y_axis: Y-Achse (volume, trade_count, avg_price)
|
||||||
|
- group_by: Gruppierung (exchange, isin, date)
|
||||||
|
- exchanges: Komma-separierte Liste von Exchanges (optional)
|
||||||
|
"""
|
||||||
|
# Validiere Parameter
|
||||||
|
valid_x_axis = ["date", "exchange", "isin"]
|
||||||
|
valid_y_axis = ["volume", "trade_count", "avg_price"]
|
||||||
|
valid_group_by = ["exchange", "isin", "date"]
|
||||||
|
|
||||||
|
if x_axis not in valid_x_axis:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid x_axis. Must be one of: {valid_x_axis}")
|
||||||
|
if y_axis not in valid_y_axis:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid y_axis. Must be one of: {valid_y_axis}")
|
||||||
|
if group_by not in valid_group_by:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid group_by. Must be one of: {valid_group_by}")
|
||||||
|
|
||||||
|
# Baue Query auf
|
||||||
|
y_axis_map = {
|
||||||
|
"volume": "sum(price * quantity)",
|
||||||
|
"trade_count": "count(*)",
|
||||||
|
"avg_price": "avg(price)"
|
||||||
|
}
|
||||||
|
|
||||||
|
x_axis_map = {
|
||||||
|
"date": "date_trunc('day', timestamp)",
|
||||||
|
"exchange": "exchange",
|
||||||
|
"isin": "isin"
|
||||||
|
}
|
||||||
|
|
||||||
|
group_by_map = {
|
||||||
|
"exchange": "exchange",
|
||||||
|
"isin": "isin",
|
||||||
|
"date": "date_trunc('day', timestamp)"
|
||||||
|
}
|
||||||
|
|
||||||
|
y_metric = y_axis_map[y_axis]
|
||||||
|
x_label = x_axis_map[x_axis]
|
||||||
|
group_by_field = group_by_map[group_by]
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
select
|
||||||
|
{x_label} as x_value,
|
||||||
|
{group_by_field} as group_value,
|
||||||
|
{y_metric} as y_value
|
||||||
|
from trades
|
||||||
|
where timestamp >= '{date_from}'
|
||||||
|
and timestamp <= '{date_to}'
|
||||||
|
"""
|
||||||
|
|
||||||
|
if exchanges:
|
||||||
|
exchange_list = ",".join([f"'{e.strip()}'" for e in exchanges.split(",")])
|
||||||
|
query += f" and exchange in ({exchange_list})"
|
||||||
|
|
||||||
|
query += f" group by {x_label}, {group_by_field} order by {x_label} asc, {group_by_field} asc"
|
||||||
|
|
||||||
|
data = query_questdb(query, timeout=15)
|
||||||
|
return format_questdb_response(data)
|
||||||
|
|
||||||
@app.get("/api/statistics/moving-average")
|
@app.get("/api/statistics/moving-average")
|
||||||
async def get_moving_average(days: int = 7, exchange: str = None):
|
async def get_moving_average(days: int = 7, exchange: str = None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user