This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -114,168 +114,46 @@ async def get_analytics(
|
||||
"exchange_sector": f"concat({t_prefix}exchange, ' - ', coalesce({m_prefix}sector, 'Unknown'))" if needs_metadata else "'Unknown'"
|
||||
}
|
||||
|
||||
# Determine table based on granularity and needs
|
||||
# For day/month aggregation without ISIN specific filtering, use analytics_daily
|
||||
# But analytics_daily doesn't have individual ISINs (except via another table)
|
||||
# So if ISIN filter is off, use analytics_daily
|
||||
selected_metric = metrics_map.get(metric, metrics_map["volume"])
|
||||
selected_group = groups_map.get(group_by, groups_map["day"])
|
||||
|
||||
use_analytics_table = False
|
||||
query = f"select {selected_group} as label"
|
||||
|
||||
# Check if we can use the pre-aggregated table
|
||||
if not isins and not sub_group_by == "isin" and group_by != "isin" and group_by != "name":
|
||||
use_analytics_table = True
|
||||
|
||||
table_name = "analytics_daily" if use_analytics_table else "trades"
|
||||
|
||||
# If using analytics table, columns might be named differently?
|
||||
# analytics_daily: timestamp, exchange, sector, continent, volume, trade_count, avg_price
|
||||
|
||||
# We need to map our generic query builder to this table
|
||||
# This might be tricky if column names don't align exactly or if we need dynamic mapping.
|
||||
# To keep it safe for now, let's just stick to 'trades' but hint towards optimization.
|
||||
# Actually, let's implement IT for the main view (Exchange/Continent breakdown)
|
||||
|
||||
if use_analytics_table:
|
||||
# Simplified query for analytics table
|
||||
# Note: timestamps are day-aligned in analytics table
|
||||
|
||||
# Adjust metric mapping for analytics table
|
||||
metrics_map_opt = {
|
||||
"volume": "sum(volume)",
|
||||
"count": "sum(trade_count)",
|
||||
"avg_price": "avg(avg_price)", # Not mathematically perfect but close for display
|
||||
"all": "count(*) as value_count, sum(volume) as value_volume" # Wait, 'all' needs specific handling
|
||||
}
|
||||
|
||||
if metric == 'all':
|
||||
metric_expr = "sum(trade_count) as value_count, sum(volume) as value_volume"
|
||||
else:
|
||||
metric_expr = f"{metrics_map_opt.get(metric, 'sum(volume)')} as value"
|
||||
|
||||
|
||||
# Group mapping logic
|
||||
# analytics_daily has: timestamp, exchange, sector, continent
|
||||
groups_map_opt = {
|
||||
"day": "timestamp",
|
||||
"month": "date_trunc('month', timestamp)",
|
||||
"exchange": "exchange",
|
||||
"continent": "continent",
|
||||
"sector": "sector",
|
||||
"exchange_continent": "concat(exchange, ' - ', continent)",
|
||||
"exchange_sector": "concat(exchange, ' - ', sector)"
|
||||
}
|
||||
|
||||
sel_group_expr = groups_map_opt.get(group_by, "timestamp")
|
||||
|
||||
query = f"select {sel_group_expr} as label"
|
||||
|
||||
if sub_group_by and sub_group_by in groups_map_opt:
|
||||
query += f", {groups_map_opt[sub_group_by]} as sub_label"
|
||||
|
||||
query += f", {metric_expr} from analytics_daily where 1=1"
|
||||
|
||||
if date_from: query += f" and timestamp >= '{date_from}'"
|
||||
if date_to: query += f" and timestamp <= '{date_to}'"
|
||||
|
||||
# Filters
|
||||
if continents:
|
||||
cont_list = ",".join([f"'{c.strip()}'" for c in continents.split(",")])
|
||||
query += f" and continent in ({cont_list})"
|
||||
|
||||
query += f" group by {sel_group_expr}"
|
||||
if sub_group_by: query += f", {groups_map_opt[sub_group_by]}"
|
||||
|
||||
query += " order by label asc"
|
||||
|
||||
if sub_group_by and sub_group_by in groups_map:
|
||||
query += f", {groups_map[sub_group_by]} as sub_label"
|
||||
|
||||
if metric == 'all':
|
||||
query += f", count(*) as value_count, sum({t_prefix}price * {t_prefix}quantity) as value_volume from trades"
|
||||
else:
|
||||
# Fallback to RAW TRADES query (existing logic)
|
||||
# ... (keep existing logic but indented/wrapped)
|
||||
selected_metric = metrics_map.get(metric, metrics_map["volume"])
|
||||
selected_group = groups_map.get(group_by, groups_map["day"])
|
||||
|
||||
query = f"select {selected_group} as label"
|
||||
|
||||
if sub_group_by and sub_group_by in groups_map:
|
||||
query += f", {groups_map[sub_group_by]} as sub_label"
|
||||
|
||||
if metric == 'all':
|
||||
query += f", count(*) as value_count, sum({t_prefix}price * {t_prefix}quantity) as value_volume from trades"
|
||||
else:
|
||||
query += f", {selected_metric} as value from trades"
|
||||
|
||||
if needs_metadata:
|
||||
query += " t left join metadata m on t.isin = m.isin"
|
||||
|
||||
query += " where 1=1"
|
||||
|
||||
if date_from:
|
||||
query += f" and {t_prefix}timestamp >= '{date_from}'"
|
||||
if date_to:
|
||||
query += f" and {t_prefix}timestamp <= '{date_to}'"
|
||||
|
||||
if isins:
|
||||
isins_list = ",".join([f"'{i.strip()}'" for i in isins.split(",")])
|
||||
query += f" and {t_prefix}isin in ({isins_list})"
|
||||
|
||||
if continents and needs_metadata:
|
||||
cont_list = ",".join([f"'{c.strip()}'" for c in continents.split(",")])
|
||||
query += f" and {m_prefix}continent in ({cont_list})"
|
||||
|
||||
query += f" group by {selected_group}"
|
||||
if sub_group_by and sub_group_by in groups_map:
|
||||
query += f", {groups_map[sub_group_by]}"
|
||||
|
||||
query += " order by label asc"
|
||||
query += f", {selected_metric} as value from trades"
|
||||
if needs_metadata:
|
||||
query += " t left join metadata m on t.isin = m.isin"
|
||||
|
||||
query += " where 1=1"
|
||||
|
||||
if date_from:
|
||||
query += f" and {t_prefix}timestamp >= '{date_from}'"
|
||||
if date_to:
|
||||
query += f" and {t_prefix}timestamp <= '{date_to}'"
|
||||
|
||||
if isins:
|
||||
isins_list = ",".join([f"'{i.strip()}'" for i in isins.split(",")])
|
||||
query += f" and {t_prefix}isin in ({isins_list})"
|
||||
|
||||
if continents and needs_metadata:
|
||||
cont_list = ",".join([f"'{c.strip()}'" for c in continents.split(",")])
|
||||
query += f" and {m_prefix}continent in ({cont_list})"
|
||||
|
||||
query += f" group by {selected_group}"
|
||||
if sub_group_by and sub_group_by in groups_map:
|
||||
query += f", {groups_map[sub_group_by]}"
|
||||
|
||||
query += " order by label asc"
|
||||
|
||||
print(f"Executing Query: {query}")
|
||||
try:
|
||||
response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': query}, auth=DB_AUTH)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
||||
print(f"DEBUG: Query Failed: {response.text}")
|
||||
|
||||
if use_analytics_table:
|
||||
print("DEBUG: Analytics query failed, falling back to RAW trades query...")
|
||||
|
||||
selected_metric = metrics_map.get(metric, metrics_map["volume"])
|
||||
selected_group = groups_map.get(group_by, groups_map["day"])
|
||||
|
||||
raw_query = f"select {selected_group} as label"
|
||||
|
||||
if sub_group_by and sub_group_by in groups_map:
|
||||
raw_query += f", {groups_map[sub_group_by]} as sub_label"
|
||||
|
||||
if metric == 'all':
|
||||
raw_query += f", count(*) as value_count, sum({t_prefix}price * {t_prefix}quantity) as value_volume from trades"
|
||||
else:
|
||||
raw_query += f", {selected_metric} as value from trades"
|
||||
|
||||
if needs_metadata:
|
||||
raw_query += " t left join metadata m on t.isin = m.isin"
|
||||
|
||||
raw_query += " where 1=1"
|
||||
|
||||
if date_from: raw_query += f" and {t_prefix}timestamp >= '{date_from}'"
|
||||
if date_to: raw_query += f" and {t_prefix}timestamp <= '{date_to}'"
|
||||
if isins:
|
||||
isins_list = ",".join([f"'{i.strip()}'" for i in isins.split(",")])
|
||||
raw_query += f" and {t_prefix}isin in ({isins_list})"
|
||||
if continents and needs_metadata:
|
||||
cont_list = ",".join([f"'{c.strip()}'" for c in continents.split(",")])
|
||||
raw_query += f" and {m_prefix}continent in ({cont_list})"
|
||||
|
||||
raw_query += f" group by {selected_group}"
|
||||
if sub_group_by and sub_group_by in groups_map:
|
||||
raw_query += f", {groups_map[sub_group_by]}"
|
||||
|
||||
raw_query += " order by label asc"
|
||||
|
||||
print(f"Executing Fallback Query: {raw_query}")
|
||||
fb_response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': raw_query}, auth=DB_AUTH)
|
||||
if fb_response.status_code == 200:
|
||||
return fb_response.json()
|
||||
|
||||
throw_http_error(response)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -292,6 +170,104 @@ async def search_metadata(q: str):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/statistics/moving-average")
|
||||
async def get_moving_average(days: int = 7, exchange: str = None):
|
||||
"""
|
||||
Gibt Moving Average Daten für Tradezahlen und Volumen je Exchange zurück.
|
||||
Unterstützte Zeiträume: 7, 30, 42, 69, 180, 365 Tage
|
||||
"""
|
||||
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 Daten aus der vorberechneten analytics_exchange_daily Tabelle
|
||||
query = f"""
|
||||
select
|
||||
timestamp as date,
|
||||
exchange,
|
||||
trade_count,
|
||||
volume,
|
||||
ma{days}_count as ma_count,
|
||||
ma{days}_volume as ma_volume
|
||||
from analytics_exchange_daily
|
||||
where timestamp >= dateadd('d', -{days}, now())
|
||||
"""
|
||||
|
||||
if exchange:
|
||||
query += f" and exchange = '{exchange}'"
|
||||
|
||||
query += " order by date asc, exchange asc"
|
||||
|
||||
try:
|
||||
response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': query}, auth=DB_AUTH)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
throw_http_error(response)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/statistics/volume-changes")
|
||||
async def get_volume_changes(days: int = 7):
|
||||
"""
|
||||
Gibt Änderungen in Volumen und Anzahl je Exchange zurück.
|
||||
Unterstützte Zeiträume: 7, 30, 42, 69, 180, 365 Tage
|
||||
"""
|
||||
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")
|
||||
|
||||
query = f"""
|
||||
select
|
||||
timestamp as date,
|
||||
exchange,
|
||||
trade_count,
|
||||
volume,
|
||||
count_change_pct,
|
||||
volume_change_pct,
|
||||
trend
|
||||
from analytics_volume_changes
|
||||
where period_days = {days}
|
||||
order by date desc, exchange asc
|
||||
"""
|
||||
|
||||
try:
|
||||
response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': query}, auth=DB_AUTH)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
throw_http_error(response)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/statistics/stock-trends")
|
||||
async def get_stock_trends(days: int = 7, limit: int = 20):
|
||||
"""
|
||||
Gibt Trendanalyse für häufig gehandelte Aktien zurück.
|
||||
Unterstützte Zeiträume: 7, 30, 42, 69, 180, 365 Tage
|
||||
"""
|
||||
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 Top-Aktien nach Volumen für den Zeitraum
|
||||
query = f"""
|
||||
select
|
||||
timestamp as date,
|
||||
isin,
|
||||
trade_count,
|
||||
volume,
|
||||
count_change_pct,
|
||||
volume_change_pct
|
||||
from analytics_stock_trends
|
||||
where period_days = {days}
|
||||
order by volume desc
|
||||
limit {limit}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': query}, auth=DB_AUTH)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
throw_http_error(response)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
def throw_http_error(res):
|
||||
raise HTTPException(status_code=res.status_code, detail=f"QuestDB error: {res.text}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user