feat: enable unlimited historical data fetching for EIX
All checks were successful
Deployment / deploy-docker (push) Successful in 15s
All checks were successful
Deployment / deploy-docker (push) Successful in 15s
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Trading Intelligence Dashboard</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>
|
||||||
<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">
|
||||||
@@ -42,164 +42,226 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config-sidebar {
|
.config-sidebar {
|
||||||
width: 320px;
|
width: 340px;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: #38bdf8;
|
background: #38bdf8;
|
||||||
color: #0b1120;
|
color: #0b1120;
|
||||||
padding: 8px 16px;
|
padding: 10px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(56, 189, 248, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background: #7dd3fc;
|
background: #7dd3fc;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-box {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
width: 100%;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item {
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-item:hover {
|
||||||
|
background: rgba(56, 189, 248, 0.1);
|
||||||
|
color: #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
display: block;
|
||||||
|
text-xs font-bold text-slate-500 uppercase tracking-widest mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-glass {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-glass:focus {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
background: rgba(56, 189, 248, 0.05);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex min-h-screen overflow-hidden">
|
<body class="flex min-h-screen overflow-hidden">
|
||||||
|
|
||||||
<!-- Sidebar (Main Nav) -->
|
<!-- Sidebar -->
|
||||||
<aside class="w-64 glass m-4 mr-0 flex flex-col hidden lg:flex">
|
<aside class="w-20 lg:w-64 glass m-4 mr-0 flex flex-col hidden sm:flex">
|
||||||
<div class="p-6">
|
<div class="p-6 hidden lg:block">
|
||||||
<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</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex-1 px-4 space-y-1 mt-4" id="sidebar">
|
<nav class="flex-1 px-4 space-y-2 mt-4" id="sidebar">
|
||||||
<a href="#" onclick="showView('dashboard')" id="nav-dashboard"
|
<a href="#" onclick="showView('dashboard')" id="nav-dashboard"
|
||||||
class="flex items-center p-3 rounded-xl transition active-nav"><span class="mr-3">📊</span>
|
class="flex items-center p-3 rounded-xl transition active-nav"><span class="mr-3">📊</span> <span
|
||||||
Dashboard</a>
|
class="hidden lg:inline">Dashboard</span></a>
|
||||||
<a href="#" onclick="showView('analytics')" id="nav-analytics"
|
<a href="#" onclick="showView('analytics')" id="nav-analytics"
|
||||||
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">📈</span> Reports (GC Style)</a>
|
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">📈</span> <span
|
||||||
|
class="hidden lg:inline">Report Builder</span></a>
|
||||||
<a href="#" onclick="showView('metadata')" id="nav-metadata"
|
<a href="#" onclick="showView('metadata')" id="nav-metadata"
|
||||||
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">🏢</span> Companies</a>
|
class="flex items-center p-3 rounded-xl transition"><span class="mr-3">🏢</span> <span
|
||||||
|
class="hidden lg:inline">Companies</span></a>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="p-6 mt-auto hidden lg:block">
|
||||||
|
<div class="glass p-4 text-xs text-sky-400 border-sky-500/30">Status: <span
|
||||||
|
class="text-green-400 animate-pulse">Connected</span></div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content -->
|
||||||
<main class="flex-1 flex flex-col min-w-0">
|
<main class="flex-1 flex flex-col min-w-0">
|
||||||
<header class="p-8 pb-0 flex justify-between items-center">
|
<header class="p-8 pb-4 flex justify-between items-center flex-shrink-0">
|
||||||
<div id="viewHeader">
|
<div>
|
||||||
<h2 class="text-3xl font-bold">Market Intelligence</h2>
|
<h2 class="text-3xl font-bold" id="pageTitle">Market Overview</h2>
|
||||||
<p class="text-slate-400 mt-1">Cross-exchange analysis and real-time visualization.</p>
|
<div id="activeIsins" class="flex flex-wrap gap-2 mt-2"></div>
|
||||||
</div>
|
|
||||||
<div id="globalControls" class="flex items-center space-x-4">
|
|
||||||
<div id="activeIsins" class="flex -space-x-2"></div>
|
|
||||||
<button onclick="fetchData()" class="glass p-2 px-4 text-sky-400 hover:text-sky-300">↻ Refresh</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button onclick="fetchData()" class="glass p-2 px-6 text-sky-400 font-bold hover:text-sky-200 transition">↻
|
||||||
|
REFRESH</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- DASHBOARD VIEW -->
|
<!-- DASHBOARD VIEW -->
|
||||||
<div id="view-dashboard" class="view flex-1 p-8 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-6 border-l-4 border-sky-500">
|
<div class="glass p-8 border-l-4 border-sky-400 glow">
|
||||||
<p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Transaction Volume</p>
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Volume (7d)</p>
|
||||||
<h3 id="statVolume" class="text-4xl font-bold">--</h3>
|
<h3 id="statVolume" class="text-4xl font-bold">€0.0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass p-6 border-l-4 border-amber-500">
|
<div class="glass p-8 border-l-4 border-amber-400">
|
||||||
<p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Total Executions</p>
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">Total Trades</p>
|
||||||
<h3 id="statTrades" class="text-4xl font-bold">--</h3>
|
<h3 id="statTrades" class="text-4xl font-bold">0</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass p-6 border-l-4 border-emerald-500">
|
<div class="glass p-8 border-l-4 border-emerald-400">
|
||||||
<p class="text-slate-400 text-xs uppercase tracking-widest mb-2">Unique Assets</p>
|
<p class="text-slate-500 text-xs font-bold uppercase tracking-wider mb-2">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>
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
||||||
<div class="glass p-8">
|
<div class="glass p-8">
|
||||||
<h3 class="text-xl font-bold mb-6">Price Trend (Recent)</h3>
|
<h3 class="text-lg font-bold mb-6 text-slate-300">Live Price Feed</h3>
|
||||||
<div class="h-80"><canvas id="priceChart"></canvas></div>
|
<div class="h-80"><canvas id="priceChart"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="glass p-8">
|
<div class="glass p-8">
|
||||||
<h3 class="text-xl font-bold mb-6">Market Distribution</h3>
|
<h3 class="text-lg font-bold mb-6 text-slate-300">Regional Distribution</h3>
|
||||||
<div class="h-80"><canvas id="continentChart"></canvas></div>
|
<div class="h-80"><canvas id="continentChart"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ANALYTICS VIEW (Google Cloud Billing Style) -->
|
<!-- REPORT BUILDER VIEW -->
|
||||||
<div id="view-analytics" class="view hidden flex-1 flex overflow-hidden">
|
<div id="view-analytics" class="view hidden flex-1 flex overflow-hidden">
|
||||||
<!-- Configuration Sidebar -->
|
<div class="config-sidebar p-8 space-y-8 overflow-y-auto bg-slate-900/30">
|
||||||
<div class="config-sidebar p-6 space-y-8 overflow-y-auto bg-slate-900/20">
|
<h3 class="text-sky-400 font-bold text-xs uppercase tracking-tighter mb-4">Report Configuration</h3>
|
||||||
|
|
||||||
|
<!-- Time Selection -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Time
|
<label class="field-label">Time Period</label>
|
||||||
Range</label>
|
<select id="timeRangePreset" class="input-glass mb-2" onchange="handlePresetChange()">
|
||||||
<div class="space-y-2">
|
<option value="1">Today</option>
|
||||||
<select id="timeRangePreset" class="w-full glass p-2 text-sm outline-none"
|
<option value="7" selected>Last 7 Days</option>
|
||||||
onchange="handlePresetChange()">
|
<option value="30">Last 30 Days</option>
|
||||||
<option value="1">1 Day</option>
|
<option value="ytd">Year to Date (YTD)</option>
|
||||||
<option value="7" selected>Last 7 Days</option>
|
<option value="year">Full Year 2026</option>
|
||||||
<option value="30">Last 30 Days</option>
|
<option value="custom">Custom Date Range...</option>
|
||||||
<option value="ytd">YTD (Year to Date)</option>
|
</select>
|
||||||
<option value="year">Current Year</option>
|
<div id="customDates" class="hidden grid grid-cols-2 gap-2 mt-2">
|
||||||
<option value="custom">Custom Range</option>
|
<input type="date" id="dateFrom" class="input-glass text-xs p-2">
|
||||||
</select>
|
<input type="date" id="dateTo" class="input-glass text-xs p-2">
|
||||||
<div id="customDates" class="hidden space-y-2 pt-2">
|
|
||||||
<input type="date" id="dateFrom" class="w-full glass p-2 text-xs">
|
|
||||||
<input type="date" id="dateTo" class="w-full glass p-2 text-xs">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dimension Selection (X) -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Group by
|
<label class="field-label">Group By (X-Axis)</label>
|
||||||
(X-Axis)</label>
|
<select id="axisX" class="input-glass" onchange="updateSubGroupOptions()">
|
||||||
<select id="axisX" class="w-full glass p-2 text-sm outline-none">
|
<option value="day">Time (Daily)</option>
|
||||||
<option value="day">Day</option>
|
<option value="month">Time (Monthly)</option>
|
||||||
<option value="month">Month</option>
|
<option value="exchange">Exchange Origin</option>
|
||||||
<option value="exchange">Exchange</option>
|
<option value="continent">Region/Continent</option>
|
||||||
<option value="continent">Continent</option>
|
<option value="sector">Industry Sector</option>
|
||||||
<option value="sector">Sector</option>
|
<option value="isin">Specific ISIN</option>
|
||||||
<option value="name">Company Name</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- Breakdown Selection (Series) -->
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Breakdown by
|
<div id="subGroupContainer">
|
||||||
(Series)</label>
|
<label class="field-label">Breakdown by (Series)</label>
|
||||||
<select id="axisSub" class="w-full glass p-2 text-sm outline-none">
|
<select id="axisSub" class="input-glass">
|
||||||
<option value="">None</option>
|
<option value="">None (Single Series)</option>
|
||||||
<option value="exchange">Exchange</option>
|
<option value="exchange">Exchange</option>
|
||||||
<option value="continent">Continent</option>
|
<option value="continent">Continent</option>
|
||||||
<option value="sector">Sector</option>
|
<option value="sector">Sector</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Metric Selection (Y) -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Metric
|
<label class="field-label">Measurement (Y-Axis)</label>
|
||||||
(Y-Axis)</label>
|
<select id="axisY" class="input-glass">
|
||||||
<select id="axisY" class="w-full glass p-2 text-sm outline-none">
|
|
||||||
<option value="volume">Trade Volume (€)</option>
|
<option value="volume">Trade Volume (€)</option>
|
||||||
<option value="count">Trade Count</option>
|
<option value="count">Number of Trades</option>
|
||||||
<option value="avg_price">Avg. Price</option>
|
<option value="avg_price">Average Price</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- Search & Filter -->
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase tracking-widest mb-4">Filters</label>
|
<div class="relative">
|
||||||
<input type="text" id="isinFilter" placeholder="Filter ISINs (split by ,)"
|
<label class="field-label">Filter to Companies</label>
|
||||||
class="w-full glass p-2 text-xs outline-none">
|
<input type="text" id="isinSearch" autocomplete="off" placeholder="Search ISIN or Name..."
|
||||||
|
class="input-glass" onkeyup="handleSearch(event)">
|
||||||
|
<div id="suggestions" class="suggestion-box hidden"></div>
|
||||||
|
<div id="filterChips" class="flex flex-wrap gap-2 mt-4"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick="renderAnalyticsReport()" class="btn-primary w-full">Run Report</button>
|
<div class="pt-4">
|
||||||
|
<button onclick="renderAnalyticsReport()" class="btn-primary w-full">GENERATE REPORT</button>
|
||||||
|
<p class="text-[10px] text-slate-500 mt-4 text-center">Charts are automatically optimized (Line vs
|
||||||
|
Bar) based on selected grouping.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Report Content Area -->
|
<div class="flex-1 p-8 overflow-hidden flex flex-col">
|
||||||
<div class="flex-1 p-8 overflow-y-auto">
|
<div class="glass p-10 h-full flex flex-col shadow-2xl relative overflow-hidden">
|
||||||
<div class="glass p-8 h-full flex flex-col">
|
<div class="flex justify-between items-center mb-10 z-10">
|
||||||
<div class="flex justify-between items-center mb-8">
|
<h3 class="text-2xl font-bold" id="reportTitle">Report: Market Activity</h3>
|
||||||
<h3 class="text-xl font-bold" id="reportTitle">Custom Trade Analysis</h3>
|
<div class="flex glass p-1 rounded-lg">
|
||||||
<div class="flex space-x-2">
|
<button class="p-2 px-4 text-xs font-bold rounded-md hover:bg-white/5"
|
||||||
<button class="glass p-2 px-3 text-xs" onclick="setChartType('line')">Line</button>
|
onclick="setChartType('line')">Line</button>
|
||||||
<button class="glass p-2 px-3 text-xs" onclick="setChartType('bar')">Bar</button>
|
<button class="p-2 px-4 text-xs font-bold rounded-md hover:bg-white/5"
|
||||||
|
onclick="setChartType('bar')">Bar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0"><canvas id="analyticsChart"></canvas></div>
|
<div class="flex-1 min-h-0 z-10"><canvas id="analyticsChart"></canvas></div>
|
||||||
|
<!-- Subtle watermark -->
|
||||||
|
<div class="absolute bottom-8 right-8 text-white/5 text-6xl font-black pointer-events-none">
|
||||||
|
ANALYTICS</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,12 +269,13 @@
|
|||||||
<!-- COMPANIES VIEW -->
|
<!-- COMPANIES VIEW -->
|
||||||
<div id="view-metadata" class="view hidden flex-1 p-8 overflow-y-auto">
|
<div id="view-metadata" class="view hidden flex-1 p-8 overflow-y-auto">
|
||||||
<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-8 border-b border-white/5 flex justify-between items-center">
|
||||||
<h3 class="text-xl font-bold text-sky-400">ISIN Metadata Directory</h3>
|
<h3 class="text-xl font-bold">Metadata Repository</h3>
|
||||||
<input type="text" id="metadataSearch" onkeyup="filterMetadata()" placeholder="Search..."
|
<input type="text" onkeyup="filterMetadata(this.value)"
|
||||||
class="glass px-4 py-2 text-sm w-80 outline-none">
|
placeholder="Search company, ISIN, sector..."
|
||||||
|
class="glass p-3 px-6 text-sm w-96 outline-none focus:border-sky-500">
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-left text-sm">
|
<table class="w-full text-left">
|
||||||
<tbody id="metadataRows"></tbody>
|
<tbody id="metadataRows"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,12 +287,17 @@
|
|||||||
let store = { trades: [], metadata: [], summary: [], pinnedIsins: [] };
|
let store = { trades: [], metadata: [], summary: [], pinnedIsins: [] };
|
||||||
let charts = {};
|
let charts = {};
|
||||||
let currentChartType = 'bar';
|
let currentChartType = 'bar';
|
||||||
|
let searchTimeout;
|
||||||
|
|
||||||
function showView(viewId) {
|
function showView(viewId) {
|
||||||
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
|
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
|
||||||
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', 'analytics': 'Custom Report Builder', 'metadata': 'Entity Metadata' };
|
||||||
|
document.getElementById('pageTitle').innerText = titles[viewId];
|
||||||
|
|
||||||
if (viewId === 'analytics') renderAnalyticsReport(); else fetchData();
|
if (viewId === 'analytics') renderAnalyticsReport(); else fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,16 +306,76 @@
|
|||||||
document.getElementById('customDates').classList.toggle('hidden', v !== 'custom');
|
document.getElementById('customDates').classList.toggle('hidden', v !== 'custom');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSubGroupOptions() {
|
||||||
|
const x = document.getElementById('axisX').value;
|
||||||
|
const sub = document.getElementById('axisSub');
|
||||||
|
const container = document.getElementById('subGroupContainer');
|
||||||
|
|
||||||
|
// Contextual Logic: If X is already a metadata field, don't allow it as series (too complex)
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
Array.from(sub.options).forEach(opt => {
|
||||||
|
opt.disabled = (opt.value === x);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearch(e) {
|
||||||
|
const q = e.target.value;
|
||||||
|
const sBox = document.getElementById('suggestions');
|
||||||
|
if (q.length < 2) { sBox.classList.add('hidden'); return; }
|
||||||
|
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
const res = await fetch(`${API}/metadata/search?q=${q}`).then(r => r.json());
|
||||||
|
const data = res.dataset || [];
|
||||||
|
if (data.length > 0) {
|
||||||
|
sBox.innerHTML = data.map(item => `<div class="suggestion-item" onclick="addFilter('${item[0]}', '${item[1]}')">
|
||||||
|
<div class="font-bold">${item[0]}</div>
|
||||||
|
<div class="text-[10px] text-slate-500">${item[1]}</div>
|
||||||
|
</div>`).join('');
|
||||||
|
sBox.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
sBox.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFilter(isin, name) {
|
||||||
|
if (!store.pinnedIsins.find(p => p.isin === isin)) {
|
||||||
|
store.pinnedIsins.push({ isin, name });
|
||||||
|
updateFilterChips();
|
||||||
|
// Close search
|
||||||
|
document.getElementById('isinSearch').value = '';
|
||||||
|
document.getElementById('suggestions').classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFilter(isin) {
|
||||||
|
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
|
||||||
|
updateFilterChips();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterChips() {
|
||||||
|
const container = document.getElementById('filterChips');
|
||||||
|
const pins = document.getElementById('activeIsins');
|
||||||
|
const html = store.pinnedIsins.map(p => `
|
||||||
|
<div class="glass p-1 px-3 text-[10px] font-bold text-sky-400 border-sky-400/30 flex items-center bg-sky-400/10">
|
||||||
|
${p.isin} <span class="ml-2 cursor-pointer text-slate-500 hover:text-white" onclick="removeFilter('${p.isin}')">×</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
container.innerHTML = html;
|
||||||
|
pins.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
function getDates() {
|
function getDates() {
|
||||||
const preset = document.getElementById('timeRangePreset').value;
|
const preset = document.getElementById('timeRangePreset').value;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let from, to = now.toISOString().split('T')[0];
|
let from, to = now.toISOString().split('T')[0];
|
||||||
|
|
||||||
if (preset === '1') from = new Date(now.setDate(now.getDate() - 1)).toISOString().split('T')[0];
|
if (preset === '1') from = to;
|
||||||
else if (preset === '7') from = new Date(now.setDate(now.getDate() - 7)).toISOString().split('T')[0];
|
else if (preset === '7') from = new Date(now.setDate(now.getDate() - 7)).toISOString().split('T')[0];
|
||||||
else if (preset === '30') from = new Date(now.setDate(now.getDate() - 30)).toISOString().split('T')[0];
|
else if (preset === '30') from = new Date(now.setDate(now.getDate() - 30)).toISOString().split('T')[0];
|
||||||
else if (preset === 'ytd') from = new Date(now.getFullYear(), 0, 1).toISOString().split('T')[0];
|
else if (preset === 'ytd') from = new Date(now.getFullYear(), 0, 1).toISOString().split('T')[0];
|
||||||
else if (preset === 'year') { from = new Date(now.getFullYear(), 0, 1).toISOString().split('T')[0]; to = new Date(now.getFullYear(), 11, 31).toISOString().split('T')[0]; }
|
else if (preset === 'year') { from = '2026-01-01'; to = '2026-12-31'; }
|
||||||
else if (preset === 'custom') { from = document.getElementById('dateFrom').value; to = document.getElementById('dateTo').value; }
|
else if (preset === 'custom') { from = document.getElementById('dateFrom').value; to = document.getElementById('dateTo').value; }
|
||||||
return { from, to };
|
return { from, to };
|
||||||
}
|
}
|
||||||
@@ -261,16 +389,16 @@
|
|||||||
]);
|
]);
|
||||||
store = { ...store, trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [] };
|
store = { ...store, trades: t.dataset || [], metadata: m.dataset || [], summary: s.dataset || [] };
|
||||||
updateDashboard();
|
updateDashboard();
|
||||||
fillMetadataTable();
|
|
||||||
} catch (err) { console.error(err); }
|
} catch (err) { console.error(err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDashboard() {
|
function updateDashboard() {
|
||||||
let vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[4]) * parseFloat(r[5] || 0)), 0);
|
let vol = store.trades.reduce((acc, r) => acc + (parseFloat(r[4]) * parseFloat(r[5] || 0)), 0);
|
||||||
document.getElementById('statVolume').innerText = vol >= 1e6 ? `€${(vol / 1e6).toFixed(2)}M` : `€${(vol / 1e3).toFixed(1)}k`;
|
document.getElementById('statVolume').innerText = vol >= 1e6 ? `€${(vol / 1e6).toFixed(1)}M` : `€${(vol / 1e3).toFixed(0)}k`;
|
||||||
document.getElementById('statTrades').innerText = store.trades.length.toLocaleString();
|
document.getElementById('statTrades').innerText = store.trades.length.toLocaleString();
|
||||||
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
|
document.getElementById('statIsins').innerText = store.metadata.length.toLocaleString();
|
||||||
renderDashboardCharts();
|
renderDashboardCharts();
|
||||||
|
fillMetadataTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
|
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
|
||||||
@@ -280,7 +408,7 @@
|
|||||||
const x = document.getElementById('axisX').value;
|
const x = document.getElementById('axisX').value;
|
||||||
const sub = document.getElementById('axisSub').value;
|
const sub = document.getElementById('axisSub').value;
|
||||||
const y = document.getElementById('axisY').value;
|
const y = document.getElementById('axisY').value;
|
||||||
const isins = document.getElementById('isinFilter').value;
|
const isins = store.pinnedIsins.map(p => p.isin).join(',');
|
||||||
|
|
||||||
let url = `${API}/analytics?metric=${y}&group_by=${x}`;
|
let url = `${API}/analytics?metric=${y}&group_by=${x}`;
|
||||||
if (sub) url += `&sub_group_by=${sub}`;
|
if (sub) url += `&sub_group_by=${sub}`;
|
||||||
@@ -293,23 +421,22 @@
|
|||||||
const data = res.dataset || [];
|
const data = res.dataset || [];
|
||||||
const ctx = document.getElementById('analyticsChart').getContext('2d');
|
const ctx = document.getElementById('analyticsChart').getContext('2d');
|
||||||
|
|
||||||
// Grouping logic for stacked/multi-series charts
|
|
||||||
let labels = [...new Set(data.map(r => r[0]))];
|
let labels = [...new Set(data.map(r => r[0]))];
|
||||||
let datasets = [];
|
if (x === 'day' || x === 'month') labels.sort((a, b) => new Date(a) - new Date(b));
|
||||||
|
|
||||||
|
let datasets = [];
|
||||||
if (sub) {
|
if (sub) {
|
||||||
let series = [...new Set(data.map(r => r[1]))];
|
let seriesNames = [...new Set(data.map(r => r[1]))];
|
||||||
series.forEach((s, idx) => {
|
seriesNames.forEach((name, idx) => {
|
||||||
// Use a wider golden-angle based hue distribution for maximum contrast
|
|
||||||
const hue = (idx * 137.5) % 360;
|
const hue = (idx * 137.5) % 360;
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: s,
|
label: name,
|
||||||
data: labels.map(l => {
|
data: labels.map(l => {
|
||||||
let match = data.find(r => r[0] === l && r[1] === s);
|
const row = data.find(r => r[0] === l && r[1] === name);
|
||||||
return match ? match[2] : 0;
|
return row ? row[2] : 0;
|
||||||
}),
|
}),
|
||||||
backgroundColor: `hsla(${hue}, 70%, 55%, 0.8)`,
|
backgroundColor: `hsla(${hue}, 75%, 50%, 0.8)`,
|
||||||
borderColor: `hsla(${hue}, 70%, 55%, 1)`,
|
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: currentChartType === 'line'
|
fill: currentChartType === 'line'
|
||||||
});
|
});
|
||||||
@@ -317,7 +444,10 @@
|
|||||||
} else {
|
} else {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: y,
|
label: y,
|
||||||
data: data.map(r => r[1]),
|
data: labels.map(l => {
|
||||||
|
const row = data.find(r => r[0] === l);
|
||||||
|
return row ? row[1] : 0;
|
||||||
|
}),
|
||||||
backgroundColor: '#38bdf888',
|
backgroundColor: '#38bdf888',
|
||||||
borderColor: '#38bdf8',
|
borderColor: '#38bdf8',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -328,63 +458,53 @@
|
|||||||
if (charts.analytics) charts.analytics.destroy();
|
if (charts.analytics) charts.analytics.destroy();
|
||||||
charts.analytics = new Chart(ctx, {
|
charts.analytics = new Chart(ctx, {
|
||||||
type: currentChartType,
|
type: currentChartType,
|
||||||
data: { labels: labels.map(l => x === 'day' ? new Date(l).toLocaleDateString() : l), datasets },
|
data: { labels: labels.map(l => (x === 'day' || x === 'month') ? new Date(l).toLocaleDateString() : l), datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: true, maintainAspectRatio: false,
|
responsive: true, maintainAspectRatio: false,
|
||||||
scales: { y: { stacked: true, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { stacked: true, grid: { display: false } } },
|
scales: { y: { stacked: true, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { stacked: true, grid: { display: false } } },
|
||||||
plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } }
|
plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.getElementById('reportTitle').innerText = `Analysis: ${y} by ${x} ${sub ? ' (Splitted by ' + sub + ')' : ''}`;
|
||||||
} catch (err) { console.error(err); }
|
} catch (err) { console.error(err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDashboardCharts() {
|
function renderDashboardCharts() {
|
||||||
const trendCtx = document.getElementById('priceChart').getContext('2d');
|
const trendCtx = document.getElementById('priceChart').getContext('2d');
|
||||||
const samples = [...store.trades].sort((a, b) => new Date(a[3]) - new Date(b[3])).slice(-50);
|
const samples = [...store.trades].sort((a, b) => new Date(a[3]) - new Date(b[3])).slice(-100);
|
||||||
if (charts.price) charts.price.destroy();
|
if (charts.price) charts.price.destroy();
|
||||||
charts.price = new Chart(trendCtx, {
|
charts.price = new Chart(trendCtx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { labels: samples.map(r => new Date(r[3]).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })), datasets: [{ data: samples.map(r => r[4]), borderColor: '#38bdf8', tension: 0.4, pointRadius: 0 }] },
|
data: { labels: samples.map(r => new Date(r[3]).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })), datasets: [{ data: samples.map(r => r[4]), borderColor: '#38bdf8', borderWidth: 2, tension: 0.4, pointRadius: 0 }] },
|
||||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { grid: { color: 'rgba(255,255,255,0.05)' } } } }
|
||||||
});
|
});
|
||||||
|
|
||||||
const contCtx = document.getElementById('continentChart').getContext('2d');
|
const contCtx = document.getElementById('continentChart').getContext('2d');
|
||||||
if (charts.continent) charts.continent.destroy();
|
if (charts.continent) charts.continent.destroy();
|
||||||
charts.continent = new Chart(contCtx, {
|
charts.continent = new Chart(contCtx, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: { labels: store.summary.map(r => r[0]), datasets: [{ data: store.summary.map(r => r[2]), backgroundColor: ['#38bdf8', '#fbbf24', '#f43f5e', '#10b981', '#8b5cf6'], borderWidth: 0 }] },
|
||||||
labels: store.summary.map(r => r[0]),
|
options: { responsive: true, maintainAspectRatio: false, cutout: '75%', plugins: { legend: { position: 'right', labels: { color: '#94a3b8' } } } }
|
||||||
datasets: [{
|
|
||||||
data: store.summary.map(r => r[2]),
|
|
||||||
backgroundColor: [
|
|
||||||
'#38bdf8', // Sky
|
|
||||||
'#f43f5e', // Rose
|
|
||||||
'#fbbf24', // Amber
|
|
||||||
'#10b981', // Emerald
|
|
||||||
'#8b5cf6', // Violet
|
|
||||||
'#f97316', // Orange
|
|
||||||
'#06b6d4', // Cyan
|
|
||||||
'#ec4899' // Pink
|
|
||||||
],
|
|
||||||
borderWidth: 0
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillMetadataTable() {
|
function fillMetadataTable() {
|
||||||
const tbody = document.getElementById('metadataRows');
|
const tbody = document.getElementById('metadataRows');
|
||||||
tbody.innerHTML = store.metadata.map(r => `
|
tbody.innerHTML = store.metadata.map(r => `
|
||||||
<tr class="hover:bg-white/5 transition border-b border-white/5">
|
<tr class="hover:bg-white/5 transition border-b border-white/5 cursor-pointer" onclick="addFilter('${r[0]}', '${r[1]}'); showView('analytics');">
|
||||||
<td class="p-4 px-6 font-mono text-sky-400 font-bold">${r[0]}</td>
|
<td class="p-5 px-8 font-mono text-sky-400 font-bold">${r[0]}</td>
|
||||||
<td class="p-4 font-semibold">${r[1]}</td>
|
<td class="p-5 font-semibold text-slate-200">${r[1]}</td>
|
||||||
<td class="p-4 text-slate-500">${r[2]}</td>
|
<td class="p-5 text-slate-500">${r[2]}</td>
|
||||||
<td class="p-4 text-slate-400 text-xs italic">${r[4]}</td>
|
<td class="p-5"><span class="bg-white/5 px-2 py-1 rounded text-[10px] text-slate-400 font-bold uppercase">${r[4] || 'UNKNOWN'}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterMetadata(q) {
|
||||||
|
const rows = document.querySelectorAll('#metadataRows tr');
|
||||||
|
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
|
||||||
|
}
|
||||||
|
|
||||||
window.onload = () => { fetchData(); setInterval(fetchData, 30000); };
|
window.onload = () => { fetchData(); setInterval(fetchData, 30000); };
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -138,6 +138,18 @@ async def get_analytics(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get("/api/metadata/search")
|
||||||
|
async def search_metadata(q: str):
|
||||||
|
# Case-insensitive search for ISIN or Name
|
||||||
|
query = f"select isin, name from metadata where isin ilike '%{q}%' or name ilike '%{q}%' limit 10"
|
||||||
|
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):
|
def throw_http_error(res):
|
||||||
raise HTTPException(status_code=res.status_code, detail=f"QuestDB error: {res.text}")
|
raise HTTPException(status_code=res.status_code, detail=f"QuestDB error: {res.text}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user