This commit is contained in:
@@ -70,9 +70,39 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="glass p-8 mb-8">
|
<div class="mb-8">
|
||||||
<h3 class="text-lg font-bold mb-6 text-slate-300">Moving Average: Tradezahlen & Volumen (alle Exchanges)</h3>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="glass p-8 mb-8">
|
<div class="glass p-8 mb-8">
|
||||||
@@ -242,153 +272,168 @@
|
|||||||
return;
|
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 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 countIdx = columns.findIndex(c => c.name === 'trade_count');
|
||||||
const volumeIdx = columns.findIndex(c => c.name === 'volume');
|
const volumeIdx = columns.findIndex(c => c.name === 'volume');
|
||||||
|
|
||||||
// Alle Daten nach Datum aggregieren (über alle Exchanges summieren)
|
// Alle Exchanges mit Canvas-IDs definieren
|
||||||
const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
|
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)
|
// Alle Daten nach Datum sortieren
|
||||||
const dailyTotals = {};
|
const dates = [...new Set(data.map(r => r[dateIdx]))].sort();
|
||||||
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)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Moving Average berechnen (7-Tage gleitender Durchschnitt)
|
// Moving Average berechnen (7-Tage gleitender Durchschnitt)
|
||||||
const maWindow = 7;
|
const maWindow = 7;
|
||||||
const tradeCountData = dates.map(d => dailyTotals[d].tradeCount);
|
|
||||||
const volumeData = dates.map(d => dailyTotals[d].volume);
|
|
||||||
|
|
||||||
const calculateMA = (data, window) => {
|
const calculateMA = (dataArray, window) => {
|
||||||
return data.map((val, idx) => {
|
return dataArray.map((val, idx) => {
|
||||||
if (idx < window - 1) return null;
|
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;
|
return slice.reduce((a, b) => a + b, 0) / window;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const maTradeCount = calculateMA(tradeCountData, maWindow);
|
// Für jede Exchange-Gruppe einen separaten Chart erstellen
|
||||||
const maVolume = calculateMA(volumeData, maWindow);
|
Object.entries(exchangeGroups).forEach(([groupName, config]) => {
|
||||||
|
const canvas = document.getElementById(config.canvasId);
|
||||||
const datasets = [
|
if (!canvas) {
|
||||||
{
|
console.error(`Canvas element ${config.canvasId} not found`);
|
||||||
label: 'Trades (täglich)',
|
return;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
// Zerstöre existierenden Chart
|
||||||
charts.movingAverage = new Chart(ctx, {
|
if (charts[config.canvasId]) charts[config.canvasId].destroy();
|
||||||
type: 'line',
|
|
||||||
data: {
|
const ctx = canvas.getContext('2d');
|
||||||
labels: dates.map(d => new Date(d).toLocaleDateString()),
|
|
||||||
datasets: datasets
|
// Aggregiere Daten für diese Gruppe
|
||||||
},
|
const groupData = {};
|
||||||
options: {
|
dates.forEach(date => {
|
||||||
responsive: true,
|
const dayRows = data.filter(r => {
|
||||||
maintainAspectRatio: false,
|
const exchange = r[exchangeIdx];
|
||||||
interaction: { mode: 'index', intersect: false },
|
return r[dateIdx] === date && config.exchanges.includes(exchange);
|
||||||
scales: {
|
});
|
||||||
y: {
|
groupData[date] = {
|
||||||
type: 'linear',
|
tradeCount: dayRows.reduce((sum, r) => sum + (r[countIdx] || 0), 0),
|
||||||
display: true,
|
volume: dayRows.reduce((sum, r) => sum + (r[volumeIdx] || 0), 0)
|
||||||
position: 'left',
|
};
|
||||||
title: { display: true, text: 'Anzahl Trades', color: '#94a3b8' },
|
});
|
||||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
|
||||||
ticks: { color: '#64748b' }
|
const tradeData = dates.map(d => groupData[d]?.tradeCount || 0);
|
||||||
},
|
const volumeData = dates.map(d => groupData[d]?.volume || 0);
|
||||||
y1: {
|
const maTradeData = calculateMA(tradeData, maWindow);
|
||||||
type: 'linear',
|
const maVolumeData = calculateMA(volumeData, maWindow);
|
||||||
display: true,
|
|
||||||
position: 'right',
|
charts[config.canvasId] = new Chart(ctx, {
|
||||||
title: { display: true, text: 'Volumen (€)', color: '#94a3b8' },
|
type: 'line',
|
||||||
grid: { drawOnChartArea: false },
|
data: {
|
||||||
ticks: {
|
labels: dates.map(d => new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })),
|
||||||
color: '#64748b',
|
datasets: [
|
||||||
callback: function(value) {
|
{
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(1) + 'M';
|
label: 'Trades (MA)',
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(0) + 'k';
|
data: maTradeData,
|
||||||
return value;
|
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: {
|
plugins: {
|
||||||
grid: { display: false },
|
legend: {
|
||||||
ticks: { color: '#64748b', maxRotation: 45 }
|
display: true,
|
||||||
}
|
position: 'top',
|
||||||
},
|
labels: { color: '#94a3b8', boxWidth: 10, usePointStyle: true, padding: 8, font: { size: 10 } }
|
||||||
plugins: {
|
},
|
||||||
legend: {
|
tooltip: {
|
||||||
display: true,
|
callbacks: {
|
||||||
position: 'bottom',
|
label: function(context) {
|
||||||
labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 }
|
let label = context.dataset.label || '';
|
||||||
},
|
if (label) label += ': ';
|
||||||
tooltip: {
|
if (context.parsed.y !== null) {
|
||||||
callbacks: {
|
if (context.dataset.yAxisID === 'y1') {
|
||||||
label: function(context) {
|
label += '€' + context.parsed.y.toLocaleString();
|
||||||
let label = context.dataset.label || '';
|
} else {
|
||||||
if (label) label += ': ';
|
label += context.parsed.y.toLocaleString();
|
||||||
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) {
|
} catch (err) {
|
||||||
console.error('Error loading moving average:', err);
|
console.error('Error loading moving average:', err);
|
||||||
|
|||||||
@@ -348,9 +348,9 @@ async def get_volume_changes(days: int = 7):
|
|||||||
if days not in [7, 30, 42, 69, 180, 365]:
|
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")
|
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"""
|
query = f"""
|
||||||
select
|
select
|
||||||
timestamp as date,
|
|
||||||
exchange,
|
exchange,
|
||||||
trade_count,
|
trade_count,
|
||||||
volume,
|
volume,
|
||||||
@@ -359,11 +359,74 @@ async def get_volume_changes(days: int = 7):
|
|||||||
trend
|
trend
|
||||||
from analytics_volume_changes
|
from analytics_volume_changes
|
||||||
where period_days = {days}
|
where period_days = {days}
|
||||||
and timestamp >= dateadd('d', -{days}, now())
|
order by timestamp desc
|
||||||
order by date desc, exchange asc
|
limit 20
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = query_questdb(query, timeout=5)
|
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)
|
return format_questdb_response(data)
|
||||||
|
|
||||||
@app.get("/api/statistics/stock-trends")
|
@app.get("/api/statistics/stock-trends")
|
||||||
|
|||||||
Reference in New Issue
Block a user