Add new feature to handle user input
This commit is contained in:
@@ -0,0 +1,854 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crypto Radar</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #0a0e17;
|
||||
--surface: #111827;
|
||||
--surface2: #1a2235;
|
||||
--border: #1e2d45;
|
||||
--text: #e2e8f0;
|
||||
--muted: #64748b;
|
||||
--accent: #3b82f6;
|
||||
--accent-dim: #1d4ed8;
|
||||
--green: #10b981;
|
||||
--green-dim: #064e3b;
|
||||
--red: #ef4444;
|
||||
--red-dim: #450a0a;
|
||||
--yellow: #f59e0b;
|
||||
--mono: 'IBM Plex Mono', monospace;
|
||||
--sans: 'IBM Plex Sans', sans-serif;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: var(--sans);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ─── Layout ─── */
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-rows: 56px 1fr;
|
||||
grid-template-columns: 280px 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ─── Header ─── */
|
||||
header {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: var(--mono);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
}
|
||||
.logo span { color: var(--accent); }
|
||||
|
||||
.header-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px; height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 6px var(--green);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 6px 14px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--sans);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.refresh-btn:hover { border-color: var(--accent); }
|
||||
|
||||
/* ─── Sidebar ─── */
|
||||
aside {
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
overflow-y: auto;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.sidebar-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
display: block;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
border-left: 2px solid transparent;
|
||||
transition: background 0.1s, border-color 0.1s;
|
||||
}
|
||||
.asset-card:hover { background: var(--surface2); }
|
||||
.asset-card.active { border-left-color: var(--accent); background: var(--surface2); }
|
||||
|
||||
.asset-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-price {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.asset-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.score-bar-wrap {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.score-bar {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 0.4s;
|
||||
}
|
||||
|
||||
.score-val {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ─── Main content ─── */
|
||||
main {
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ─── Summary strip ─── */
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
font-family: var(--mono);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-sub {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ─── Content grid ─── */
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ─── Panel ─── */
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-family: var(--mono);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.panel-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.panel-body { padding: 16px; }
|
||||
|
||||
/* ─── Chart ─── */
|
||||
.chart-wrap {
|
||||
position: relative;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
/* ─── Indicators grid ─── */
|
||||
.indicators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.indicator-cell {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.ind-label {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ind-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.ind-signal {
|
||||
font-size: 10px;
|
||||
margin-top: 3px;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* ─── Signal pill ─── */
|
||||
.pill {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
font-family: var(--mono);
|
||||
padding: 2px 7px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pill-green { background: var(--green-dim); color: var(--green); }
|
||||
.pill-red { background: var(--red-dim); color: var(--red); }
|
||||
.pill-yellow { background: #292001; color: var(--yellow); }
|
||||
.pill-blue { background: #0f1e3d; color: var(--accent); }
|
||||
|
||||
/* ─── Sentiment panel ─── */
|
||||
.sentiment-score-large {
|
||||
font-family: var(--mono);
|
||||
font-size: 42px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sentiment-bar-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
margin: 12px 0;
|
||||
position: relative;
|
||||
}
|
||||
.sentiment-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
/* ─── News list ─── */
|
||||
.news-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
.news-item {
|
||||
padding: 10px 12px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--border);
|
||||
}
|
||||
.news-item.pos { border-left-color: var(--green); }
|
||||
.news-item.neg { border-left-color: var(--red); }
|
||||
.news-item.neu { border-left-color: var(--muted); }
|
||||
|
||||
.news-title {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.news-title a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.news-title a:hover { color: var(--accent); }
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* ─── Reason box ─── */
|
||||
.reason-box {
|
||||
padding: 10px 12px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ─── Change colors ─── */
|
||||
.pos-text { color: var(--green); }
|
||||
.neg-text { color: var(--red); }
|
||||
|
||||
/* ─── Loading / empty ─── */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ─── Tradeable badge ─── */
|
||||
.tradeable-yes { color: var(--green); font-family: var(--mono); font-size: 11px; }
|
||||
.tradeable-no { color: var(--red); font-family: var(--mono); font-size: 11px; }
|
||||
|
||||
/* scrollbar */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="shell">
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="logo">crypto<span>.</span>radar</div>
|
||||
<div class="header-meta">
|
||||
<div class="pulse-dot"></div>
|
||||
<span class="timestamp" id="ts">--:--:--</span>
|
||||
<button class="refresh-btn" onclick="loadRanked()">Ververs</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside>
|
||||
<div class="sidebar-label">TOP 6 ASSETS</div>
|
||||
<div id="sidebar-list">
|
||||
<div class="loading">laden...</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main>
|
||||
<!-- Summary strip -->
|
||||
<div class="summary-strip">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">geselecteerd</div>
|
||||
<div class="stat-value" id="sum-symbol">—</div>
|
||||
<div class="stat-sub" id="sum-tradeable">—</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">prijs (EUR)</div>
|
||||
<div class="stat-value" id="sum-price">—</div>
|
||||
<div class="stat-sub" id="sum-change"></div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">opportunity score</div>
|
||||
<div class="stat-value" id="sum-score">—</div>
|
||||
<div class="stat-sub" id="sum-trend">—</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">sentiment</div>
|
||||
<div class="stat-value" id="sum-sentiment">—</div>
|
||||
<div class="stat-sub" id="sum-sent-label">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart + sentiment -->
|
||||
<div class="content-grid">
|
||||
<!-- Left: chart + indicators -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" id="chart-title">GRAFIEK</span>
|
||||
<span class="panel-subtitle">5m candles</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="chart-wrap">
|
||||
<canvas id="priceChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">INDICATOREN</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="indicators-grid" id="indicators-grid">
|
||||
<div class="loading">selecteer een asset</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">SIGNAAL REDEN</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="reason-box" id="reason-box">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: sentiment + news -->
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">SENTIMENT</span>
|
||||
<span class="panel-subtitle" id="news-count">0 artikelen</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="sentiment-score-large" id="sent-score-large">—</div>
|
||||
<div style="font-size:11px;color:var(--muted);font-family:var(--mono)" id="sent-label-large">—</div>
|
||||
<div class="sentiment-bar-track">
|
||||
<div class="sentiment-bar-fill" id="sent-bar" style="width:50%;background:var(--muted)"></div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--muted);font-family:var(--mono)">
|
||||
<span>negatief</span><span>neutraal</span><span>positief</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="flex:1;">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">NIEUWS</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="news-list" id="news-list">
|
||||
<div class="loading">selecteer een asset</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ─── State ───────────────────────────────────────────
|
||||
let rankedData = [];
|
||||
let activeSymbol = null;
|
||||
let priceChart = null;
|
||||
|
||||
// ─── API base — zelfde origin via nginx proxy ─────────
|
||||
const API = '/api';
|
||||
|
||||
// ─── Timestamp tick ──────────────────────────────────
|
||||
setInterval(() => {
|
||||
document.getElementById('ts').textContent =
|
||||
new Date().toLocaleTimeString('nl-NL', { hour12: false });
|
||||
}, 1000);
|
||||
|
||||
// ─── Initialiseer chart ───────────────────────────────
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('priceChart').getContext('2d');
|
||||
priceChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Sluitprijs',
|
||||
data: [],
|
||||
borderColor: '#3b82f6',
|
||||
borderWidth: 1.5,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
backgroundColor: '#111827',
|
||||
borderColor: '#1e2d45',
|
||||
borderWidth: 1,
|
||||
titleColor: '#64748b',
|
||||
bodyColor: '#e2e8f0',
|
||||
titleFont: { family: 'IBM Plex Mono', size: 10 },
|
||||
bodyFont: { family: 'IBM Plex Mono', size: 11 },
|
||||
callbacks: {
|
||||
label: ctx => `€ ${ctx.parsed.y.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 4})}`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: '#1e2d45' },
|
||||
ticks: { color: '#64748b', font: { family: 'IBM Plex Mono', size: 9 }, maxTicksLimit: 8 },
|
||||
},
|
||||
y: {
|
||||
position: 'right',
|
||||
grid: { color: '#1e2d45' },
|
||||
ticks: { color: '#64748b', font: { family: 'IBM Plex Mono', size: 9 },
|
||||
callback: v => '€' + v.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 4})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Laad ranked assets ───────────────────────────────
|
||||
async function loadRanked() {
|
||||
try {
|
||||
const res = await fetch(`${API}/assets/ranked`);
|
||||
const json = await res.json();
|
||||
rankedData = json.data || [];
|
||||
renderSidebar(rankedData);
|
||||
|
||||
// Selecteer eerste asset automatisch
|
||||
if (rankedData.length > 0 && !activeSymbol) {
|
||||
selectAsset(rankedData[0].symbol);
|
||||
} else if (activeSymbol) {
|
||||
loadDetail(activeSymbol);
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('sidebar-list').innerHTML =
|
||||
`<div class="loading">fout: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Render sidebar ───────────────────────────────────
|
||||
function renderSidebar(assets) {
|
||||
const el = document.getElementById('sidebar-list');
|
||||
if (!assets.length) {
|
||||
el.innerHTML = '<div class="loading">geen data</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = assets.map(a => {
|
||||
const changeClass = a.change_24h >= 0 ? 'pos-text' : 'neg-text';
|
||||
const changeSign = a.change_24h >= 0 ? '+' : '';
|
||||
const coin = a.symbol.split('/')[0];
|
||||
return `
|
||||
<div class="asset-card ${a.symbol === activeSymbol ? 'active' : ''}"
|
||||
onclick="selectAsset('${a.symbol}')">
|
||||
<div class="asset-row">
|
||||
<span class="asset-name">
|
||||
<span class="rank-badge">#${a.rank}</span>${coin}
|
||||
</span>
|
||||
<span class="asset-price ${changeClass}">
|
||||
${changeSign}${a.change_24h.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="asset-meta">
|
||||
<div class="score-bar-wrap">
|
||||
<div class="score-bar" style="width:${a.combined_score}%"></div>
|
||||
</div>
|
||||
<span class="score-val">${a.combined_score}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ─── Selecteer asset ──────────────────────────────────
|
||||
function selectAsset(symbol) {
|
||||
activeSymbol = symbol;
|
||||
renderSidebar(rankedData);
|
||||
loadDetail(symbol);
|
||||
}
|
||||
|
||||
// ─── Laad detail ─────────────────────────────────────
|
||||
async function loadDetail(symbol) {
|
||||
const encodedSymbol = symbol.replace('/', '%2F');
|
||||
|
||||
// Summary strip alvast bijwerken met ranked data
|
||||
const rankedAsset = rankedData.find(a => a.symbol === symbol);
|
||||
if (rankedAsset) updateSummaryFromRanked(rankedAsset);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/assets/${encodedSymbol}`);
|
||||
const json = await res.json();
|
||||
const d = json.data;
|
||||
|
||||
updateSummaryFull(symbol, d);
|
||||
updateChart(symbol, d.candles);
|
||||
updateIndicators(d.indicators, d.ta_score, rankedAsset);
|
||||
updateSentiment(d.sentiment);
|
||||
updateNews(d.sentiment.news);
|
||||
|
||||
document.getElementById('reason-box').textContent =
|
||||
rankedAsset ? rankedAsset.reason : '—';
|
||||
document.getElementById('chart-title').textContent =
|
||||
`${symbol.replace('/', '/EUR — ')} PRIJS`;
|
||||
|
||||
} catch(e) {
|
||||
console.error('Detail load fout:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Summary updates ─────────────────────────────────
|
||||
function updateSummaryFromRanked(a) {
|
||||
const coin = a.symbol.split('/')[0];
|
||||
document.getElementById('sum-symbol').textContent = coin;
|
||||
document.getElementById('sum-score').textContent = a.combined_score;
|
||||
document.getElementById('sum-trend').textContent = `trend: ${a.trend_strength}`;
|
||||
document.getElementById('sum-sentiment').textContent = a.sentiment_score.toFixed(2);
|
||||
document.getElementById('sum-sent-label').textContent = a.sentiment_label;
|
||||
|
||||
const trEl = document.getElementById('sum-tradeable');
|
||||
trEl.className = a.tradeable ? 'tradeable-yes' : 'tradeable-no';
|
||||
trEl.textContent = a.tradeable ? '✓ tradeable' : '✗ niet tradeable';
|
||||
}
|
||||
|
||||
function updateSummaryFull(symbol, d) {
|
||||
const price = d.price || 0;
|
||||
const change = d.change_24h || 0;
|
||||
const changeEl = document.getElementById('sum-change');
|
||||
|
||||
document.getElementById('sum-price').textContent =
|
||||
'€ ' + price.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 4});
|
||||
|
||||
changeEl.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}% (24u)`;
|
||||
changeEl.className = change >= 0 ? 'pos-text' : 'neg-text';
|
||||
}
|
||||
|
||||
// ─── Chart update ─────────────────────────────────────
|
||||
function updateChart(symbol, candles) {
|
||||
if (!candles || !candles.length) return;
|
||||
|
||||
const labels = candles.map(c => {
|
||||
const d = new Date(c.time);
|
||||
return d.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
|
||||
});
|
||||
const prices = candles.map(c => c.close);
|
||||
|
||||
priceChart.data.labels = labels;
|
||||
priceChart.data.datasets[0].data = prices;
|
||||
priceChart.data.datasets[0].label = symbol;
|
||||
priceChart.update('none');
|
||||
}
|
||||
|
||||
// ─── Indicators update ────────────────────────────────
|
||||
function updateIndicators(ind, taScore, ranked) {
|
||||
const el = document.getElementById('indicators-grid');
|
||||
if (!ind) { el.innerHTML = '<div class="loading">geen data</div>'; return; }
|
||||
|
||||
const rsiClass = ind.rsi < 30 ? 'pill-yellow' :
|
||||
ind.rsi > 70 ? 'pill-red' : 'pill-green';
|
||||
|
||||
const emaClass = ranked && ranked.ema_signal === 'bullish' ? 'pill-green' :
|
||||
ranked && ranked.ema_signal === 'bearish' ? 'pill-red' : 'pill-yellow';
|
||||
|
||||
const vwapClass = ranked && ranked.vwap_signal === 'onder_vwap' ? 'pill-green' : 'pill-blue';
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="indicator-cell">
|
||||
<div class="ind-label">RSI (14)</div>
|
||||
<div class="ind-value">${ind.rsi ? ind.rsi.toFixed(1) : '—'}</div>
|
||||
<div class="ind-signal">
|
||||
<span class="pill ${rsiClass}">${ranked ? ranked.rsi_signal : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="indicator-cell">
|
||||
<div class="ind-label">EMA 9 / 21</div>
|
||||
<div class="ind-value" style="font-size:12px">
|
||||
${ind.ema9 ? ind.ema9.toFixed(2) : '—'}<br>
|
||||
<span style="color:var(--muted);font-size:11px">${ind.ema21 ? ind.ema21.toFixed(2) : '—'}</span>
|
||||
</div>
|
||||
<div class="ind-signal">
|
||||
<span class="pill ${emaClass}">${ranked ? ranked.ema_signal : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="indicator-cell">
|
||||
<div class="ind-label">VWAP</div>
|
||||
<div class="ind-value" style="font-size:13px">${ind.vwap ? ind.vwap.toFixed(2) : '—'}</div>
|
||||
<div class="ind-signal">
|
||||
<span class="pill ${vwapClass}">${ranked ? ranked.vwap_signal : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="indicator-cell">
|
||||
<div class="ind-label">Bollinger positie</div>
|
||||
<div class="ind-value">${ind.bb_position ? (ind.bb_position * 100).toFixed(0) + '%' : '—'}</div>
|
||||
<div class="ind-signal" style="font-size:10px;color:var(--muted)">0% = lower band</div>
|
||||
</div>
|
||||
<div class="indicator-cell">
|
||||
<div class="ind-label">ATR %</div>
|
||||
<div class="ind-value ${ind.atr_pct > 5 ? 'neg-text' : 'pos-text'}">
|
||||
${ind.atr_pct ? ind.atr_pct.toFixed(2) + '%' : '—'}
|
||||
</div>
|
||||
<div class="ind-signal" style="font-size:10px;color:var(--muted)">
|
||||
${ind.atr_pct > 5 ? 'te volatiel' : 'acceptabel'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="indicator-cell">
|
||||
<div class="ind-label">TA score</div>
|
||||
<div class="ind-value ${taScore > 60 ? 'pos-text' : taScore > 40 ? '' : 'neg-text'}">${taScore || '—'}</div>
|
||||
<div class="ind-signal" style="font-size:10px;color:var(--muted)">van 100</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ─── Sentiment update ─────────────────────────────────
|
||||
function updateSentiment(s) {
|
||||
if (!s) return;
|
||||
|
||||
const score = s.score;
|
||||
const scoreEl = document.getElementById('sent-score-large');
|
||||
scoreEl.textContent = score.toFixed(2);
|
||||
scoreEl.style.color = score > 0.15 ? 'var(--green)' :
|
||||
score < -0.15 ? 'var(--red)' : 'var(--yellow)';
|
||||
|
||||
document.getElementById('sent-label-large').textContent =
|
||||
`${s.label} · confidence ${(s.confidence * 100).toFixed(0)}%`;
|
||||
|
||||
document.getElementById('news-count').textContent = `${s.news_count} artikelen`;
|
||||
|
||||
// Bar: score -1→+1 naar 0→100%
|
||||
const pct = ((score + 1) / 2) * 100;
|
||||
const bar = document.getElementById('sent-bar');
|
||||
bar.style.width = pct + '%';
|
||||
bar.style.background = score > 0.15 ? 'var(--green)' :
|
||||
score < -0.15 ? 'var(--red)' : 'var(--yellow)';
|
||||
}
|
||||
|
||||
// ─── News update ──────────────────────────────────────
|
||||
function updateNews(news) {
|
||||
const el = document.getElementById('news-list');
|
||||
if (!news || !news.length) {
|
||||
el.innerHTML = '<div style="color:var(--muted);font-size:12px;font-family:var(--mono)">geen nieuws beschikbaar</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = news.map(n => {
|
||||
const cls = n.sentiment === 'positive' ? 'pos' :
|
||||
n.sentiment === 'negative' ? 'neg' : 'neu';
|
||||
const pill = n.sentiment === 'positive' ? 'pill-green' :
|
||||
n.sentiment === 'negative' ? 'pill-red' : 'pill-yellow';
|
||||
const time = new Date(n.published).toLocaleString('nl-NL', {
|
||||
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="news-item ${cls}">
|
||||
<div class="news-title">
|
||||
<a href="${n.url}" target="_blank" rel="noopener">${n.title}</a>
|
||||
</div>
|
||||
<div class="news-meta">
|
||||
<span class="pill ${pill}">${n.sentiment}</span>
|
||||
<span>${n.source}</span>
|
||||
<span>${time}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ─── Auto-refresh elke 60s ────────────────────────────
|
||||
setInterval(loadRanked, 60_000);
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────
|
||||
initChart();
|
||||
loadRanked();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user