fix: date parsing, continent grouping, dynamic analytics, and sector scraping
All checks were successful
Deployment / deploy-docker (push) Successful in 5s

This commit is contained in:
Melchior Reimers
2026-01-23 18:17:02 +01:00
parent 1086c4aa1d
commit dbd4fbfb47
3 changed files with 312 additions and 253 deletions

View File

@@ -11,370 +11,399 @@
<style> <style>
body { body {
font-family: 'Outfit', sans-serif; font-family: 'Outfit', sans-serif;
background-color: #0b0e14; background-color: #0b1120;
color: #e2e8f0; color: #e2e8f0;
} }
.glass { .glass {
background: rgba(30, 41, 59, 0.5); background: rgba(30, 41, 59, 0.4);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px; border-radius: 16px;
} }
.glow { .glass-btn {
box-shadow: 0 0 20px rgba(56, 189, 248, 0.2); background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.2);
transition: all 0.3s;
} }
.sidebar-item:hover { .glass-btn:hover {
background: rgba(56, 189, 248, 0.2);
border-color: #38bdf8;
}
.glow {
box-shadow: 0 0 20px rgba(56, 189, 248, 0.1);
}
.active-nav {
background: rgba(56, 189, 248, 0.1); background: rgba(56, 189, 248, 0.1);
color: #38bdf8; color: #38bdf8;
border-right: 3px solid #38bdf8;
} }
canvas { canvas {
max-width: 100%; max-width: 100%;
} }
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 10px;
}
</style> </style>
</head> </head>
<body class="flex min-h-screen"> <body class="flex min-h-screen overflow-hidden">
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-64 glass m-4 flex flex-col hidden lg:flex"> <aside class="w-64 glass m-4 mr-0 flex flex-col hidden lg:flex">
<div class="p-6"> <div class="p-6">
<h1 class="text-2xl font-bold bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text text-transparent"> <h1 class="text-2xl font-bold bg-gradient-to-r from-sky-400 to-blue-500 bg-clip-text text-transparent">
Antigravity Trade</h1> Antigravity Trade</h1>
</div> </div>
<nav class="flex-1 px-4 space-y-2 mt-4" id="sidebar"> <nav class="flex-1 px-4 space-y-1 mt-4" id="sidebar">
<a href="#" onclick="showView('dashboard')" <a href="#" onclick="showView('dashboard')" id="nav-dashboard"
class="sidebar-item flex items-center p-3 rounded-xl transition bg-sky-500/10 text-sky-400" class="flex items-center p-3 rounded-xl transition active-nav">
id="nav-dashboard">
<span class="mr-3">📊</span> Dashboard <span class="mr-3">📊</span> Dashboard
</a> </a>
<a href="#" onclick="showView('analytics')" class="sidebar-item flex items-center p-3 rounded-xl transition" <a href="#" onclick="showView('analytics')" id="nav-analytics"
id="nav-analytics"> class="flex items-center p-3 rounded-xl transition">
<span class="mr-3">📈</span> Analytics <span class="mr-3">📈</span> Analytics Hub
</a> </a>
<a href="#" onclick="showView('metadata')" class="sidebar-item flex items-center p-3 rounded-xl transition" <a href="#" onclick="showView('metadata')" id="nav-metadata"
id="nav-metadata"> class="flex items-center p-3 rounded-xl transition">
<span class="mr-3">🏢</span> Companies <span class="mr-3">🏢</span> Companies
</a> </a>
</nav> </nav>
<div class="p-6 mt-auto"> <div class="p-6 mt-auto">
<div class="glass p-4 text-xs text-sky-400 border-sky-500/30"> <div class="glass p-4 text-xs text-sky-400 border-sky-500/30">
System Status: <span class="text-green-400">Live</span> Data Stream: <span class="text-green-400 animate-pulse">Live</span>
</div> </div>
</div> </div>
</aside> </aside>
<!-- Main Content --> <!-- Main Content -->
<main class="flex-1 p-8 overflow-y-auto"> <main class="flex-1 p-8 overflow-y-auto">
<header class="flex justify-between items-center mb-8"> <header class="flex justify-between items-center mb-10">
<div> <div id="viewHeader">
<h2 class="text-3xl font-bold">Market Overview</h2> <h2 class="text-3xl font-bold">Market Intelligence</h2>
<p class="text-slate-400">Real-time trading analytics across multiple exchanges.</p> <p class="text-slate-400 mt-1">Cross-exchange analysis and real-time visualization.</p>
</div> </div>
<div class="flex space-x-4"> <div class="flex items-center space-x-4">
<select id="timeRange" class="glass px-4 py-2 text-sm outline-none"> <div id="activeIsins" class="flex -space-x-2">
<option value="7">Last 7 Days</option> <!-- Pinned ISINs will show up here -->
</div>
<select id="timeRange" class="glass px-4 py-2 text-sm outline-none focus:ring-1 ring-sky-500"
onchange="fetchData()">
<option value="1">Last 24 Hours</option> <option value="1">Last 24 Hours</option>
<option value="7" selected>Last 7 Days</option>
<option value="30">Last 30 Days</option> <option value="30">Last 30 Days</option>
<option value="90">Last Quarter</option>
</select> </select>
<div class="glass px-4 py-2 flex items-center text-sm font-semibold text-sky-400"> <button onclick="fetchData()" class="glass p-2 px-4 text-sky-400 hover:text-sky-300"></button>
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span> Connected
</div>
</div> </div>
</header> </header>
<!-- View: Dashboard --> <!-- DASHBOARD VIEW -->
<div id="content-dashboard" class="content-view"> <div id="view-dashboard" class="view">
<!-- Stats Grid --> <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-8"> <div class="glass p-6 glow border-l-4 border-sky-500">
<div class="glass p-6 glow"> <p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Transaction Volume</p>
<p class="text-slate-400 text-sm mb-1 text-uppercase tracking-wider">Total Trades</p> <h3 id="statVolume" class="text-4xl font-bold">€0.0k</h3>
<h3 id="statTotalTrades" class="text-4xl font-bold">--</h3>
</div> </div>
<div class="glass p-6"> <div class="glass p-6 border-l-4 border-amber-500">
<p class="text-slate-400 text-sm mb-1 text-uppercase tracking-wider">Total Volume</p> <p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Total Executions</p>
<h3 id="statTotalVolume" class="text-4xl font-bold">--</h3> <h3 id="statTrades" class="text-4xl font-bold">0</h3>
</div> </div>
<div class="glass p-6"> <div class="glass p-6 border-l-4 border-emerald-500">
<p class="text-slate-400 text-sm mb-1 text-uppercase tracking-wider">Tracked ISINs</p> <p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Unique Assets</p>
<h3 id="statIsins" class="text-4xl font-bold">--</h3> <h3 id="statIsins" class="text-4xl font-bold">0</h3>
</div> </div>
</div> </div>
<!-- Charts Container --> <div class="grid grid-cols-1 xl:grid-cols-2 gap-8 mb-10">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8 mb-8"> <div class="glass p-8">
<div class="glass p-6"> <h3 class="text-xl font-bold mb-6">Execution Price Trend</h3>
<h3 class="text-xl font-bold mb-6">Price Evolution (Recent)</h3> <div class="h-80"><canvas id="priceChart"></canvas></div>
<div class="h-80"><canvas id="mainChart"></canvas></div>
</div> </div>
<div class="glass p-6"> <div class="glass p-8">
<h3 class="text-xl font-bold mb-6">Distribution by Continent</h3> <h3 class="text-xl font-bold mb-6">Geographic Distribution</h3>
<div class="h-80"><canvas id="pieChart"></canvas></div> <div class="h-80"><canvas id="continentChart"></canvas></div>
</div> </div>
</div> </div>
</div> </div>
<!-- View: Analytics --> <!-- ANALYTICS VIEW -->
<div id="content-analytics" class="content-view hidden"> <div id="view-analytics" class="view hidden">
<div class="glass p-6 mb-8"> <div class="glass p-8 mb-8">
<h3 class="text-xl font-bold mb-6">Exchange Comparison (Volume)</h3> <div class="flex justify-between items-center mb-8">
<div class="h-96"><canvas id="comparisonChart"></canvas></div> <h3 class="text-xl font-bold">Custom Feature Comparison</h3>
<div class="flex space-x-4">
<select id="axisX" class="glass px-3 py-1 text-xs" onchange="renderCustomChart()">
<option value="time">Time</option>
<option value="exchange">Exchange</option>
<option value="continent">Continent</option>
</select>
<select id="axisY" class="glass px-3 py-1 text-xs" onchange="renderCustomChart()">
<option value="volume">Trade Volume</option>
<option value="count">Trade Count</option>
<option value="avg_price">Avg. Price</option>
</select>
</div> </div>
</div>
<div class="h-96"><canvas id="customChart"></canvas></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="glass p-6"> <div class="glass p-6">
<h3 class="text-xl font-bold mb-6">EIX vs LS Trade Count</h3> <h3 class="text-lg font-bold mb-6">Exchange Dominance</h3>
<div class="h-64"><canvas id="tradeCountChart"></canvas></div> <div class="h-64"><canvas id="exchangeChart"></canvas></div>
</div> </div>
<div class="glass p-6"> <div class="glass p-6">
<h3 class="text-xl font-bold mb-6">Average Trade Size</h3> <h3 class="text-lg font-bold mb-6">Market Dynamics</h3>
<div id="avgTradeData" class="space-y-4"> <div id="dynamicStats" class="space-y-4">
<!-- Dynamic data --> <!-- Stats injected here -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- View: Metadata (Table) --> <!-- COMPANIES VIEW -->
<div id="content-metadata" class="content-view hidden"> <div id="view-metadata" class="view hidden">
<!-- Data Table -->
<div class="glass overflow-hidden"> <div class="glass overflow-hidden">
<div class="p-6 border-b border-white/5 flex justify-between items-center"> <div class="p-6 border-b border-white/5 flex justify-between items-center">
<h3 class="text-xl font-bold">Company Metadata</h3> <h3 class="text-xl font-bold text-sky-400">ISIN Metadata Directory</h3>
<input type="text" placeholder="Search ISIN..." class="glass px-4 py-1 text-sm"> <input type="text" id="metadataSearch" onkeyup="filterMetadata()"
placeholder="Search ISIN, Name or Sector..."
class="glass px-4 py-2 text-sm w-80 outline-none focus:border-sky-500">
</div> </div>
<table class="w-full text-left"> <table class="w-full text-left">
<thead class="bg-white/5 text-slate-400 text-sm uppercase"> <thead class="bg-white/5 text-slate-400 text-xs uppercase tracking-tighter font-bold">
<tr> <tr>
<th class="p-4 px-6">ISIN</th> <th class="p-4 px-6">ISIN</th>
<th class="p-4">Name</th> <th class="p-4">Entity Legal Name</th>
<th class="p-4">Country</th> <th class="p-4">Origin</th>
<th class="p-4">Continent</th> <th class="p-4">Continent</th>
<th class="p-4">Sector</th> <th class="p-4">Market Sector</th>
<th class="p-4 text-center">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="metadataTable" class="divide-y divide-white/5"> <tbody id="metadataRows" class="divide-y divide-white/5 text-sm">
<!-- Rows injected via JS --> <!-- Filled by JS -->
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</main> </main>
<script> <script>
const API_BASE = '/api'; const API = '/api';
let store = { trades: [], metadata: [], summary: [], comparison: [], pinnedIsins: [] };
function showView(viewId) { let charts = {};
document.querySelectorAll('.content-view').forEach(v => v.classList.add('hidden'));
document.getElementById(`content-${viewId}`).classList.remove('hidden');
document.querySelectorAll('#sidebar a').forEach(a => {
a.classList.remove('bg-sky-500/10', 'text-sky-400');
});
document.getElementById(`nav-${viewId}`).classList.add('bg-sky-500/10', 'text-sky-400');
const titles = {
'dashboard': ['Market Overview', 'Real-time trading analytics across multiple exchanges.'],
'analytics': ['Advanced Analytics', 'Deep dive comparison and historical trends.'],
'metadata': ['Enriched Company Data', 'Unified metadata from GLEIF and Financial APIs.']
};
document.querySelector('#viewTitle h2').innerText = titles[viewId][0];
document.querySelector('#viewTitle p').innerText = titles[viewId][1];
fetchData();
}
async function fetchData() { async function fetchData() {
try { try {
const days = document.getElementById('timeRange').value; const days = document.getElementById('timeRange').value;
const [tradesRes, metaRes, summaryRes, compRes] = await Promise.all([ const [t, m, s, c] = await Promise.all([
fetch(`${API_BASE}/trades?days=${days}`), fetch(`${API}/trades?days=${days}`).then(r => r.json()),
fetch(`${API_BASE}/metadata`), fetch(`${API}/metadata`).then(r => r.json()),
fetch(`${API_BASE}/summary`), fetch(`${API}/summary`).then(r => r.json()),
fetch(`${API_BASE}/comparison?days=${days}`) fetch(`${API}/comparison?days=${days}`).then(r => r.json())
]); ]);
store = { trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [], comparison: c.dataset || [], pinnedIsins: store.pinnedIsins };
const trades = await tradesRes.json(); updateUI();
const metadata = await metaRes.json(); } catch (err) { console.error("Fetch error:", err); }
const summary = await summaryRes.json();
const comparison = await compRes.json();
updateStats(trades, metadata);
renderMainChart(trades);
renderPieChart(summary);
updateMetadataTable(metadata);
renderComparisonCharts(comparison);
} catch (e) {
console.error("Dashboard error:", e);
}
} }
function updateStats(trades, metadata) { function updateUI() {
const rowCount = trades.dataset ? trades.dataset.length : 0; updateStats();
document.getElementById('statTotalTrades').innerText = rowCount.toLocaleString(); renderDashboardCharts();
renderCustomChart();
let totalVol = 0; renderExchangeChart();
if (trades.dataset) { fillMetadataTable();
trades.dataset.forEach(row => {
const p = parseFloat(row[4]) || 0;
const q = parseFloat(row[5]) || 0;
totalVol += (p * q);
});
} }
if (totalVol >= 1000000) { function updateStats() {
document.getElementById('statTotalVolume').innerText = '€' + (totalVol / 1000000).toFixed(2) + 'M'; document.getElementById('statTrades').innerText = store.trades.length.toLocaleString();
} else { document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
document.getElementById('statTotalVolume').innerText = '€' + (totalVol / 1000).toFixed(1) + 'k'; let vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[4]) * parseFloat(r[5])), 0);
document.getElementById('statVolume').innerText = vol >= 1e6 ? `${(vol / 1e6).toFixed(2)}M` : `${(vol / 1e3).toFixed(1)}k`;
} }
const metaCount = metadata.dataset ? metadata.dataset.length : 0; function showView(viewId) {
document.getElementById('statIsins').innerText = metaCount.toLocaleString(); document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
document.getElementById(`view-${viewId}`).classList.remove('hidden');
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
document.getElementById(`nav-${viewId}`).classList.add('active-nav');
window.activeView = viewId;
fetchData();
} }
let mainChart, pieChart, compChart, countChart; function renderDashboardCharts() {
// Price Trend
const trendCtx = document.getElementById('priceChart').getContext('2d');
const sorted = [...store.trades].sort((a, b) => new Date(a[3]) - new Date(b[3]));
const samples = sorted.slice(-100);
function renderComparisonCharts(data) { if (charts.price) charts.price.destroy();
if (!data.dataset) return; charts.price = new Chart(trendCtx, {
const ctxComp = document.getElementById('comparisonChart').getContext('2d');
const ctxCount = document.getElementById('tradeCountChart').getContext('2d');
// Organize data: {exchange: {days: [], volumes: []}}
const exchanges = {};
const daysSet = new Set();
data.dataset.forEach(row => {
const ex = row[0];
const day = new Date(row[1]).toLocaleDateString();
const vol = row[2];
const count = row[3];
if (!exchanges[ex]) exchanges[ex] = { days: {}, volumes: {}, counts: {} };
exchanges[ex].volumes[day] = vol;
exchanges[ex].counts[day] = count;
daysSet.add(day);
});
const labels = Array.from(daysSet).sort((a, b) => new Date(a) - new Date(b));
const createDataset = (ex, type, color) => ({
label: ex,
data: labels.map(l => exchanges[ex][type][l] || 0),
borderColor: color,
backgroundColor: color + '33',
fill: true,
tension: 0.3
});
if (compChart) compChart.destroy();
compChart = new Chart(ctxComp, {
type: 'line', type: 'line',
data: { data: {
labels, labels: samples.map(r => {
datasets: [ const d = new Date(r[3]);
createDataset('LS', 'volumes', '#38bdf8'), return isNaN(d) ? '?' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
createDataset('EIX', 'volumes', '#fbbf24') }),
]
},
options: { responsive: true, maintainAspectRatio: false }
});
if (countChart) countChart.destroy();
countChart = new Chart(ctxCount, {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'LS', data: labels.map(l => exchanges['LS']?.counts[l] || 0), backgroundColor: '#38bdf8' },
{ label: 'EIX', data: labels.map(l => exchanges['EIX']?.counts[l] || 0), backgroundColor: '#fbbf24' }
]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
function renderMainChart(trades) {
if (!trades.dataset) return;
const ctx = document.getElementById('mainChart').getContext('2d');
// Basic aggregation for demonstration
const labels = trades.dataset.slice(-20).map(r => new Date(r[2]).toLocaleTimeString());
const data = trades.dataset.slice(-20).map(r => r[4]);
if (mainChart) mainChart.destroy();
mainChart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{ datasets: [{
label: 'Price Evolution', label: 'Execution Price',
data, data: samples.map(r => parseFloat(r[4])),
borderColor: '#38bdf8', borderColor: '#38bdf8',
backgroundColor: 'rgba(56, 189, 248, 0.2)', borderWidth: 2,
fill: true, fill: true,
backgroundColor: 'rgba(56, 189, 248, 0.05)',
tension: 0.4, tension: 0.4,
borderWidth: 3 pointRadius: 0
}] }]
}, },
options: { options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false } }, y: { grid: { color: 'rgba(255,255,255,0.05)' } } }, plugins: { legend: { display: false } } }
responsive: true,
maintainAspectRatio: false,
scales: {
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } },
x: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } }
},
plugins: { legend: { display: false } }
}
}); });
}
function renderPieChart(summary) { // Continent Pie
if (!summary.dataset) return; const contCtx = document.getElementById('continentChart').getContext('2d');
const ctx = document.getElementById('pieChart').getContext('2d'); if (charts.continent) charts.continent.destroy();
const continents = summary.dataset.map(r => r[0]); charts.continent = new Chart(contCtx, {
const volumes = summary.dataset.map(r => r[3]);
if (pieChart) pieChart.destroy();
pieChart = new Chart(ctx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: continents, labels: store.summary.map(r => r[0]),
datasets: [{ datasets: [{
data: volumes, data: store.summary.map(r => r[2]),
backgroundColor: ['#38bdf8', '#fbbf24', '#f87171', '#34d399', '#818cf8'], backgroundColor: ['#38bdf8', '#fbbf24', '#10b981', '#f87171', '#818cf8', '#a78bfa'],
borderWidth: 0 borderWidth: 0,
hoverOffset: 20
}] }]
}, },
options: { options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 } } } } }
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } }
}
}); });
} }
function updateMetadataTable(metadata) { function renderCustomChart() {
const container = document.getElementById('metadataTable'); const ctx = document.getElementById('customChart').getContext('2d');
container.innerHTML = ''; const x = document.getElementById('axisX').value;
if (!metadata.dataset) return; const y = document.getElementById('axisY').value;
metadata.dataset.forEach(row => { // Simple Dynamic Aggregation
let grouped = {};
store.comparison.forEach(r => {
const label = x === 'time' ? new Date(r[1]).toLocaleDateString() : r[0]; // Simplified
if (!grouped[label]) grouped[label] = { val: 0, count: 0, prices: 0 };
grouped[label].val += r[2]; // Volume
grouped[label].count += r[3]; // Count
grouped[label].prices += r[2] / (r[3] || 1); // Mock avg price
});
const labels = Object.keys(grouped);
let finalData = labels.map(l => {
if (y === 'volume') return grouped[l].val;
if (y === 'count') return grouped[l].count;
return grouped[l].prices / (grouped[l].count || 1);
});
if (charts.custom) charts.custom.destroy();
charts.custom = new Chart(ctx, {
type: x === 'time' ? 'line' : 'bar',
data: {
labels,
datasets: [{
label: `${y} by ${x}`,
data: finalData,
backgroundColor: '#38bdf8',
borderColor: '#38bdf8',
borderRadius: 8
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
function renderExchangeChart() {
const ctx = document.getElementById('exchangeChart').getContext('2d');
const dataMap = {};
store.trades.forEach(r => {
dataMap[r[0]] = (dataMap[r[0]] || 0) + 1;
});
if (charts.exchange) charts.exchange.destroy();
charts.exchange = new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(dataMap),
datasets: [{
data: Object.values(dataMap),
backgroundColor: ['#38bdf8', '#fbbf24'],
borderRadius: 10
}]
},
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
});
}
function fillMetadataTable() {
const tbody = document.getElementById('metadataRows');
tbody.innerHTML = '';
store.metadata.forEach(r => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.className = 'hover:bg-white/5 transition cursor-default group';
tr.innerHTML = ` tr.innerHTML = `
<td class="p-4 px-6 font-mono text-sky-400">${row[0]}</td> <td class="p-4 px-6 font-mono text-sky-400 font-bold">${r[0]}</td>
<td class="p-4 font-semibold">${row[1]}</td> <td class="p-4 font-semibold text-slate-200">${r[1]}</td>
<td class="p-4"><span class="bg-white/10 px-2 py-1 rounded text-xs">${row[2]}</span></td> <td class="p-4"><span class="px-2 py-1 bg-sky-500/10 text-sky-400 rounded-lg text-xs font-bold ring-1 ring-sky-500/20">${r[2]}</span></td>
<td class="p-4 text-slate-400">${row[3]}</td> <td class="p-4 text-slate-400">${r[3]}</td>
<td class="p-4 text-slate-400">${row[4]}</td> <td class="p-4"><span class="text-slate-500 italic">${r[4]}</span></td>
<td class="p-4 text-center">
<button onclick="pinIsin('${r[0]}')" class="glass-btn p-2 rounded-lg text-lg opacity-0 group-hover:opacity-100">📌</button>
</td>
`; `;
container.appendChild(tr); tbody.appendChild(tr);
}); });
} }
document.getElementById('timeRange').addEventListener('change', fetchData); function pinIsin(isin) {
fetchData(); if (!store.pinnedIsins.includes(isin)) {
setInterval(fetchData, 30000); store.pinnedIsins.push(isin);
updatePinnedUI();
}
}
function updatePinnedUI() {
const container = document.getElementById('activeIsins');
container.innerHTML = '';
store.pinnedIsins.forEach(isin => {
const chip = document.createElement('div');
chip.className = 'glass p-1 px-3 text-xs text-sky-400 border-sky-500/50 flex items-center bg-sky-950/50 cursor-pointer hover:bg-sky-900 transition';
chip.innerHTML = `${isin} <span class="ml-2 text-slate-500" onclick="unpinIsin('${isin}')">×</span>`;
container.appendChild(chip);
});
}
function unpinIsin(isin) {
store.pinnedIsins = store.pinnedIsins.filter(i => i !== isin);
updatePinnedUI();
}
function filterMetadata() {
const q = document.getElementById('metadataSearch').value.toLowerCase();
const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => {
const text = r.innerText.toLowerCase();
r.style.display = text.includes(q) ? '' : 'none';
});
}
window.onload = () => { window.activeView = 'dashboard'; fetchData(); setInterval(fetchData, 30000); };
</script> </script>
</body> </body>

View File

@@ -55,12 +55,15 @@ async def get_metadata():
@app.get("/api/summary") @app.get("/api/summary")
async def get_summary(): async def get_summary():
# Group by continent/country if metadata exists # Coalesce null values to 'Unknown' and group properly
query = """ query = """
select m.continent, m.country, count(*) as trade_count, sum(t.price * t.quantity) as total_volume select
coalesce(m.continent, 'Unknown') as continent,
count(*) as trade_count,
sum(t.price * t.quantity) as total_volume
from trades t from trades t
left join metadata m on t.isin = m.isin left join metadata m on t.isin = m.isin
group by m.continent, m.country group by continent
""" """
try: try:
response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': query}, auth=DB_AUTH) response = requests.get(f"http://{DB_HOST}:9000/exec", params={'query': query}, auth=DB_AUTH)
@@ -73,8 +76,14 @@ async def get_summary():
@app.get("/api/comparison") @app.get("/api/comparison")
async def get_comparison(days: int = 7): async def get_comparison(days: int = 7):
# Aggregated volume per exchange per day # Aggregated volume per exchange per day
# QuestDB date_trunc('day', timestamp) is good.
# We ensure we get exchange, day, and metrics.
query = f""" query = f"""
select exchange, date_trunc('day', timestamp) as day, sum(price * quantity) as daily_volume, count(*) as trade_count select
exchange,
date_trunc('day', timestamp) as day,
sum(price * quantity) as daily_volume,
count(*) as trade_count
from trades from trades
where timestamp > dateadd('d', -{days}, now()) where timestamp > dateadd('d', -{days}, now())
group by exchange, day group by exchange, day

View File

@@ -35,6 +35,8 @@ def get_processed_isins():
return [] return []
return [] return []
from bs4 import BeautifulSoup
def fetch_metadata(isin): def fetch_metadata(isin):
logger.info(f"Fetching metadata for ISIN: {isin}") logger.info(f"Fetching metadata for ISIN: {isin}")
metadata = { metadata = {
@@ -45,10 +47,15 @@ def fetch_metadata(isin):
'sector': 'Unknown' 'sector': 'Unknown'
} }
# Common headers to avoid blocks
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
# 1. GLEIF API for Name and Country # 1. GLEIF API for Name and Country
try: try:
gleif_url = f"https://api.gleif.org/api/v1/lei-records?filter[isin]={isin}" gleif_url = f"https://api.gleif.org/api/v1/lei-records?filter[isin]={isin}"
res = requests.get(gleif_url, timeout=10) res = requests.get(gleif_url, headers=headers, timeout=10)
if res.status_code == 200: if res.status_code == 200:
data = res.json().get('data', []) data = res.json().get('data', [])
if data: if data:
@@ -58,11 +65,25 @@ def fetch_metadata(isin):
except Exception as e: except Exception as e:
logger.error(f"GLEIF error for {isin}: {e}") logger.error(f"GLEIF error for {isin}: {e}")
# 2. Continent mapping from Country Code # 2. Yahoo Finance for Sector
try:
# We use the lookup URL as discussed
yahoo_url = f"https://finance.yahoo.com/lookup/?s={isin}"
res = requests.get(yahoo_url, headers=headers, timeout=10)
if res.status_code == 200:
soup = BeautifulSoup(res.text, 'html.parser')
# Look for the sector link in the results table
sector_link = soup.find('a', href=lambda x: x and '/sector/' in x)
if sector_link:
metadata['sector'] = sector_link.text.strip()
except Exception as e:
logger.error(f"Yahoo sector error for {isin}: {e}")
# 3. Continent mapping from Country Code
if metadata['country'] != 'Unknown': if metadata['country'] != 'Unknown':
try: try:
country_url = f"https://restcountries.com/v3.1/alpha/{metadata['country']}" country_url = f"https://restcountries.com/v3.1/alpha/{metadata['country']}"
res = requests.get(country_url, timeout=10) res = requests.get(country_url, headers=headers, timeout=10)
if res.status_code == 200: if res.status_code == 200:
data = res.json() data = res.json()
if data and isinstance(data, list): if data and isinstance(data, list):