diff --git a/dashboard/public/index.html b/dashboard/public/index.html
index 0acc709..21c5236 100644
--- a/dashboard/public/index.html
+++ b/dashboard/public/index.html
@@ -70,9 +70,39 @@
-
-
Moving Average: Tradezahlen & Volumen (alle Exchanges)
-
+
+
Moving Average: Tradezahlen & Volumen je Exchange
+
Blau = Trades (7-Tage MA), Grün = Volumen (7-Tage MA)
+
+
+
+
LS (Lang & Schwarz)
+
+
+
+
+
+
+
+
@@ -242,153 +272,168 @@
return;
}
- const canvas = document.getElementById('movingAverageChart');
- if (!canvas) {
- console.error('Canvas element movingAverageChart not found');
- return;
- }
- const ctx = canvas.getContext('2d');
- if (charts.movingAverage) charts.movingAverage.destroy();
-
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 Daten nach Datum aggregieren (über alle Exchanges summieren)
- const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
+ // 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' }
+ };
- // Summiere Trade Count und Volume pro Tag (alle Exchanges zusammen)
- const dailyTotals = {};
- dates.forEach(date => {
- const dayRows = data.filter(r => r[dateIdx] === date);
- dailyTotals[date] = {
- tradeCount: dayRows.reduce((sum, r) => sum + (r[countIdx] || 0), 0),
- volume: dayRows.reduce((sum, r) => sum + (r[volumeIdx] || 0), 0)
- };
- });
+ // 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 tradeCountData = dates.map(d => dailyTotals[d].tradeCount);
- const volumeData = dates.map(d => dailyTotals[d].volume);
- const calculateMA = (data, window) => {
- return data.map((val, idx) => {
+ const calculateMA = (dataArray, window) => {
+ return dataArray.map((val, idx) => {
if (idx < window - 1) return null;
- const slice = data.slice(idx - window + 1, idx + 1);
+ const slice = dataArray.slice(idx - window + 1, idx + 1);
return slice.reduce((a, b) => a + b, 0) / window;
});
};
- const maTradeCount = calculateMA(tradeCountData, maWindow);
- const maVolume = calculateMA(volumeData, maWindow);
-
- const datasets = [
- {
- label: 'Trades (täglich)',
- data: tradeCountData,
- borderColor: '#38bdf8',
- backgroundColor: '#38bdf833',
- borderWidth: 1,
- yAxisID: 'y',
- tension: 0.3,
- pointRadius: 2
- },
- {
- label: `Trades (${maWindow}-Tage MA)`,
- data: maTradeCount,
- borderColor: '#38bdf8',
- backgroundColor: 'transparent',
- borderWidth: 3,
- yAxisID: 'y',
- tension: 0.4,
- pointRadius: 0
- },
- {
- label: 'Volumen (täglich)',
- data: volumeData,
- borderColor: '#10b981',
- backgroundColor: '#10b98133',
- borderWidth: 1,
- yAxisID: 'y1',
- tension: 0.3,
- pointRadius: 2
- },
- {
- label: `Volumen (${maWindow}-Tage MA)`,
- data: maVolume,
- borderColor: '#10b981',
- backgroundColor: 'transparent',
- borderWidth: 3,
- yAxisID: 'y1',
- tension: 0.4,
- pointRadius: 0
+ // 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;
}
- ];
-
- 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: 'Anzahl Trades', color: '#94a3b8' },
- grid: { color: 'rgba(255,255,255,0.05)' },
- ticks: { color: '#64748b' }
- },
- y1: {
- type: 'linear',
- display: true,
- position: 'right',
- title: { display: true, text: 'Volumen (€)', color: '#94a3b8' },
- grid: { drawOnChartArea: false },
- ticks: {
- color: '#64748b',
- callback: function(value) {
- if (value >= 1e6) return (value / 1e6).toFixed(1) + 'M';
- if (value >= 1e3) return (value / 1e3).toFixed(0) + 'k';
- return value;
+
+ // 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
}
}
},
- x: {
- grid: { display: false },
- ticks: { color: '#64748b', maxRotation: 45 }
- }
- },
- plugins: {
- legend: {
- display: true,
- position: 'bottom',
- labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 }
- },
- 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();
+ 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;
}
- return label;
}
}
}
}
- }
+ });
});
} catch (err) {
console.error('Error loading moving average:', err);
diff --git a/dashboard/server.py b/dashboard/server.py
index c1c8abe..00ab81f 100644
--- a/dashboard/server.py
+++ b/dashboard/server.py
@@ -348,9 +348,9 @@ async def get_volume_changes(days: int = 7):
if days not in [7, 30, 42, 69, 180, 365]:
raise HTTPException(status_code=400, detail="Invalid days parameter. Must be one of: 7, 30, 42, 69, 180, 365")
+ # Hole die neuesten Daten für den angegebenen Zeitraum
query = f"""
select
- timestamp as date,
exchange,
trade_count,
volume,
@@ -359,11 +359,74 @@ async def get_volume_changes(days: int = 7):
trend
from analytics_volume_changes
where period_days = {days}
- and timestamp >= dateadd('d', -{days}, now())
- order by date desc, exchange asc
+ order by timestamp desc
+ limit 20
"""
data = query_questdb(query, timeout=5)
+
+ # Falls keine vorberechneten Daten vorhanden, berechne on-the-fly
+ if not data or not data.get('dataset'):
+ logger.info(f"No pre-calculated volume changes found for {days} days, calculating on-the-fly")
+
+ # Berechne Volumen-Änderungen direkt aus trades
+ query = f"""
+ with
+ first_half as (
+ select
+ exchange,
+ count(*) as trade_count,
+ sum(price * quantity) as volume
+ from trades
+ where timestamp >= dateadd('d', -{days}, now())
+ and timestamp < dateadd('d', -{days/2}, now())
+ group by exchange
+ ),
+ second_half as (
+ select
+ exchange,
+ count(*) as trade_count,
+ sum(price * quantity) as volume
+ from trades
+ where timestamp >= dateadd('d', -{days/2}, now())
+ group by exchange
+ )
+ select
+ coalesce(f.exchange, s.exchange) as exchange,
+ coalesce(s.trade_count, 0) as trade_count,
+ coalesce(s.volume, 0) as volume,
+ case when f.trade_count > 0 then
+ ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count)
+ else 0 end as count_change_pct,
+ case when f.volume > 0 then
+ ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume)
+ else 0 end as volume_change_pct,
+ case
+ when f.trade_count > 0 and f.volume > 0 then
+ case
+ when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) > 5
+ and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) > 5
+ then 'mehr_trades_mehr_volumen'
+ when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) > 5
+ and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) < -5
+ then 'mehr_trades_weniger_volumen'
+ when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) < -5
+ and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) > 5
+ then 'weniger_trades_mehr_volumen'
+ when ((coalesce(s.trade_count, 0) - f.trade_count) * 100.0 / f.trade_count) < -5
+ and ((coalesce(s.volume, 0) - f.volume) * 100.0 / f.volume) < -5
+ then 'weniger_trades_weniger_volumen'
+ else 'stabil'
+ end
+ else 'neu'
+ end as trend
+ from first_half f
+ full outer join second_half s on f.exchange = s.exchange
+ order by s.volume desc
+ """
+
+ data = query_questdb(query, timeout=15)
+
return format_questdb_response(data)
@app.get("/api/statistics/stock-trends")