KarlQuant commited on
Commit
67e8cc2
Β·
verified Β·
1 Parent(s): 0df5245

Update hub_dashboard.html

Browse files
Files changed (1) hide show
  1. hub_dashboard.html +40 -22
hub_dashboard.html CHANGED
@@ -2820,6 +2820,24 @@ const f4 = v => (v==null)?'β€”':Number(v).toFixed(4); const fPct = v => (v==null
2820
  const ago = ts => { if(!ts) return 'β€”'; const s=Math.round(Date.now()/1000-ts); return s<60?s+'s ago':s<3600?Math.floor(s/60)+'m ago':Math.floor(s/3600)+'h ago'; };
2821
  const stxt = r => r.signal_confidence>.7 ? '<span style="color:var(--green);font-weight:700;text-shadow:0 0 8px rgba(0,223,138,0.4)">Engaged</span>' : '<span style="color:var(--t3);font-weight:600">Standby</span>';
2822
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2823
  function scoreBar(s) {
2824
  const v=parseFloat(s)||0; const p=Math.min(Math.abs(v)*100,100).toFixed(1);
2825
  const c=v>.05?'#00df8a':v<-.05?'#ff3d5a':'#00c8ff';
@@ -2840,7 +2858,7 @@ function renderTable(){
2840
  return `<tr class="${sel} ${rcls}" onclick="onRow('${r.space_name}',this)">
2841
  <td class="rc">${rk}</td><td class="nc">${r.space_name}</td><td>${scoreBar(r.score)}</td>
2842
  <td class="num">${fPct(r.signal_confidence)}</td><td class="num">${fPct(r.avn_accuracy)}</td>
2843
- <td><span class="sig ${r.latest_signal||r.dominant_signal}">${r.latest_signal||r.dominant_signal}</span></td>
2844
  <td class="num">${(r.training_steps||0).toLocaleString()}</td><td class="num">${f4(r.actor_loss)}</td>
2845
  <td>${stxt(r)}</td></tr>`;
2846
  }).join('');
@@ -2868,7 +2886,7 @@ function onRow(name,el){
2868
  <div class="dr"><span class="dk">Model Core</span><span class="dv">QUASAR-X1</span></div>
2869
  </div>
2870
  <div><div class="dc-title">Ensemble Consensus</div>
2871
- <div class="dr"><span class="dk">Inference Signal</span><span class="dv ${(r.latest_signal||r.dominant_signal)==='BUY'?'g':(r.latest_signal||r.dominant_signal)==='SELL'?'r':''}">${r.latest_signal||r.dominant_signal}</span></div>
2872
  <div class="dr"><span class="dk">Buy Votes</span><span class="dv g">${r.buy_count}</span></div>
2873
  <div class="dr"><span class="dk">Sell Votes</span><span class="dv r">${r.sell_count}</span></div>
2874
  </div>`;
@@ -2878,7 +2896,7 @@ function onRow(name,el){
2878
  function updateCharts(){
2879
  scoreChart.data.labels=_rankings.map(r=>r.space_name);
2880
  scoreChart.data.datasets[0].data=_rankings.map(r=>r.score);
2881
- scoreChart.data.datasets[0].backgroundColor=_rankings.map(r=>{ const sig=r.latest_signal||r.dominant_signal; return sig==='BUY'?'rgba(0,223,138,0.7)': sig==='SELL'?'rgba(255,61,90,0.7)':'rgba(0,200,255,0.5)'; });
2882
  scoreChart.update();
2883
  if(!_rankings.length)return; const top=_rankings[0].space_name; const h=(_hist[top]||[]).slice(-60); if(!h.length)return;
2884
  const lb=h.map(p=>new Date(p.ts*1000).toLocaleTimeString('en-GB',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}));
@@ -3267,7 +3285,7 @@ const GATE_DEFS = [
3267
 
3268
  // Derive per-asset gate status from ranking data
3269
  function gateStatus(r) {
3270
- const A = (r.latest_signal||r.dominant_signal) === 'BUY' || (r.latest_signal||r.dominant_signal) === 'SELL';
3271
  const B = r.signal_confidence >= 0.55;
3272
  const C = Math.abs(r.score) >= 0.10;
3273
  const D = r.avn_accuracy >= 0.50;
@@ -3284,8 +3302,8 @@ function renderTradingTab() {
3284
  if (!_rankings || !_rankings.length) return;
3285
 
3286
  // ── KPI Row ────────────────────────────────────────────────────────────────
3287
- const buys = _rankings.filter(r => (r.latest_signal||r.dominant_signal) === 'BUY');
3288
- const sells = _rankings.filter(r => (r.latest_signal||r.dominant_signal) === 'SELL');
3289
  document.getElementById('t-kpi-buys').textContent = buys.length;
3290
  document.getElementById('t-kpi-sells').textContent = sells.length;
3291
 
@@ -3314,11 +3332,11 @@ function renderTradingTab() {
3314
  const allPass = allGatesPass(gates);
3315
  const gateIcon = allPass ? '<span style="color:var(--green);font-weight:800">βœ“ CLEAR</span>'
3316
  : '<span style="color:var(--amber);font-weight:700">⚠ GATED</span>';
3317
- const sigClass = (r.latest_signal||r.dominant_signal) === 'BUY' ? 'BUY' : (r.latest_signal||r.dominant_signal) === 'SELL' ? 'SELL' : 'HOLD';
3318
  return `<tr>
3319
  <td class="rc ${i===0?'r1':''}">${r.rank}</td>
3320
  <td class="nc">${r.space_name}</td>
3321
- <td><span class="sig ${sigClass}">${r.dominant_signal}</span></td>
3322
  <td class="num">${fPct(r.signal_confidence)}</td>
3323
  <td class="num" style="color:var(--green)">${r.buy_count}</td>
3324
  <td class="num" style="color:var(--red)">${r.sell_count}</td>
@@ -3348,7 +3366,7 @@ function renderTradingTab() {
3348
  const gLabel = allP
3349
  ? '<span class="pos-gate-pass">βœ“ ALL CLEAR</span>'
3350
  : `<span class="pos-gate-warn">A:${gates.A?'βœ“':'βœ—'} B:${gates.B?'βœ“':'βœ—'} C:${gates.C?'βœ“':'βœ—'} D:${gates.D?'βœ“':'βœ—'} E:${gates.E?'βœ“':'βœ—'}</span>`;
3351
- const side = r.dominant_signal === 'BUY' ? 'BUY' : 'SELL';
3352
  return `<tr>
3353
  <td class="nc">${r.space_name}</td>
3354
  <td><span class="sig ${side}">${side}</span></td>
@@ -3366,13 +3384,13 @@ function renderTradingTab() {
3366
  const total = r.buy_count + r.sell_count || 1;
3367
  const buyPct = (r.buy_count / total * 100).toFixed(0);
3368
  const sellPct = (r.sell_count / total * 100).toFixed(0);
3369
- const sigCls = (r.latest_signal||r.dominant_signal) === 'BUY' ? 'BUY' : (r.latest_signal||r.dominant_signal) === 'SELL' ? 'SELL' : 'HOLD';
3370
  const tagBg = sigCls === 'BUY' ? 'rgba(0,223,138,0.12)' : sigCls === 'SELL' ? 'rgba(255,61,90,0.12)' : 'rgba(255,255,255,0.07)';
3371
  const tagClr = sigCls === 'BUY' ? 'var(--green)' : sigCls === 'SELL' ? 'var(--red)' : 'var(--t2)';
3372
  return `<div class="vote-row">
3373
  <div class="vote-asset-label">
3374
  <span class="vote-asset-name">${r.space_name}</span>
3375
- <span class="vote-signal-tag" style="background:${tagBg};color:${tagClr};border:1px solid ${tagClr}40">${r.dominant_signal}</span>
3376
  </div>
3377
  <div class="vote-bar-track">
3378
  <div class="vote-bar-buy" style="width:${buyPct}%"></div>
@@ -3410,8 +3428,8 @@ function renderTradingTab() {
3410
  const fb = document.getElementById('trade-feed-body');
3411
  const now = Date.now();
3412
  fb.innerHTML = _rankings.slice(0, 20).map(r => {
3413
- const dir = r.dominant_signal === 'BUY' ? 'L' : r.dominant_signal === 'SELL' ? 'S' : 'N';
3414
- const dirLabel = r.dominant_signal === 'BUY' ? 'LONG' : r.dominant_signal === 'SELL' ? 'SHORT' : 'HOLD';
3415
  const ts = r.last_updated ? new Date(r.last_updated * 1000).toLocaleTimeString('en-GB', {hour12:false}) : 'β€”';
3416
  return `<div class="feed-cols row">
3417
  <div class="f-info">
@@ -3498,9 +3516,9 @@ function normalise(vals) {
3498
  // ── Summary bar ───────────────────────────────────────────────────────────────
3499
  function renderAssetSummary(assets) {
3500
  document.getElementById('as-total').textContent = assets.length;
3501
- document.getElementById('as-bull').textContent = assets.filter(r => (r.latest_signal||r.dominant_signal) === 'BUY').length;
3502
- document.getElementById('as-bear').textContent = assets.filter(r => (r.latest_signal||r.dominant_signal) === 'SELL').length;
3503
- document.getElementById('as-neut').textContent = assets.filter(r => (r.latest_signal||r.dominant_signal) !== 'BUY' && (r.latest_signal||r.dominant_signal) !== 'SELL').length;
3504
  const accs = assets.filter(r => r.avn_accuracy > 0).map(r => r.avn_accuracy);
3505
  document.getElementById('as-avgacc').textContent = accs.length
3506
  ? fPct(accs.reduce((a,b) => a+b, 0) / accs.length) : 'β€”';
@@ -3515,7 +3533,7 @@ function renderAssetCards(assets) {
3515
  return;
3516
  }
3517
  grid.innerHTML = assets.map(r => {
3518
- const sigCls = r.dominant_signal === 'BUY' ? 'sig-buy' : r.dominant_signal === 'SELL' ? 'sig-sell' : 'sig-hold';
3519
  const scoreClr = r.score > 0.1 ? 'var(--green)' : r.score < -0.05 ? 'var(--red)' : 'var(--cyan)';
3520
  const total = r.buy_count + r.sell_count || 1;
3521
  const buyPct = (r.buy_count / total * 100).toFixed(0);
@@ -3527,7 +3545,7 @@ function renderAssetCards(assets) {
3527
  <div class="ac-name">${r.space_name}</div>
3528
  <div class="ac-rank">RANK #${r.rank}</div>
3529
  </div>
3530
- <span class="sig ${r.dominant_signal === 'BUY' ? 'BUY' : r.dominant_signal === 'SELL' ? 'SELL' : 'HOLD'}">${r.dominant_signal}</span>
3531
  </div>
3532
  <div class="ac-score-block">
3533
  <div class="ac-score-label">AXRVI Score</div>
@@ -3572,12 +3590,12 @@ function renderAssetTable(assets) {
3572
  return;
3573
  }
3574
  body.innerHTML = assets.map(r => {
3575
- const sigCls = (r.latest_signal||r.dominant_signal) === 'BUY' ? 'BUY' : (r.latest_signal||r.dominant_signal) === 'SELL' ? 'SELL' : 'HOLD';
3576
  const lossClr = r.actor_loss < 0.3 ? 'var(--green)' : r.actor_loss > 0.7 ? 'var(--red)' : 'var(--amber)';
3577
  return `<tr onclick="openDeepDive('${r.space_name}')" style="cursor:pointer">
3578
  <td class="rc ${r.rank===1?'r1':''}">${r.rank}</td>
3579
  <td class="nc">${r.space_name}</td>
3580
- <td style="text-align:right"><span class="sig ${sigCls}">${r.dominant_signal}</span></td>
3581
  <td class="num" style="color:var(--cyan)">${fPct(r.signal_confidence)}</td>
3582
  <td class="num">${fPct(r.avn_accuracy)}</td>
3583
  <td class="num" style="color:${r.score>0.1?'var(--green)':r.score<0?'var(--red)':'var(--t1)'}">${f4(r.score)}</td>
@@ -3650,13 +3668,13 @@ function openDeepDive(name) {
3650
  dive.style.display = '';
3651
  dive.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
3652
 
3653
- const sigCls = r.dominant_signal === 'BUY' ? 'g' : r.dominant_signal === 'SELL' ? 'r' : '';
3654
  document.getElementById('dive-stats').innerHTML = `
3655
  <div class="dive-block">
3656
  <div class="dive-block-title">Neural Output</div>
3657
  <div class="dive-row"><span class="dive-k">AXRVI Score</span><span class="dive-v ${r.score > 0.1 ? 'g' : r.score < 0 ? 'r' : 'c'}">${f4(r.score)}</span></div>
3658
  <div class="dive-row"><span class="dive-k">Signal Confidence</span><span class="dive-v c">${fPct(r.signal_confidence)}</span></div>
3659
- <div class="dive-row"><span class="dive-k">Dominant Signal</span><span class="dive-v ${sigCls}">${r.dominant_signal}</span></div>
3660
  <div class="dive-row"><span class="dive-k">Rank</span><span class="dive-v">#${r.rank}</span></div>
3661
  <div class="dive-row"><span class="dive-k">Last Updated</span><span class="dive-v">${ago(r.last_updated)}</span></div>
3662
  </div>
 
2820
  const ago = ts => { if(!ts) return 'β€”'; const s=Math.round(Date.now()/1000-ts); return s<60?s+'s ago':s<3600?Math.floor(s/60)+'m ago':Math.floor(s/3600)+'h ago'; };
2821
  const stxt = r => r.signal_confidence>.7 ? '<span style="color:var(--green);font-weight:700;text-shadow:0 0 8px rgba(0,223,138,0.4)">Engaged</span>' : '<span style="color:var(--t3);font-weight:600">Standby</span>';
2822
 
2823
+ /* ─────────────────────────────────────────────────────────────────────────────
2824
+ vecOf(r) β€” normalize the asset's directional signal for display.
2825
+ The hub's /api/state now emits `flip_direction` (BUY|SELL|NONE). Older
2826
+ builds used `latest_signal` or `dominant_signal`. This helper reads the
2827
+ new field first, falls back to the legacy ones, and maps anything
2828
+ non-directional to 'NEUTRAL' so the CSS classes (.sig.BUY, .sig.SELL,
2829
+ .sig.NEUTRAL) and downstream comparisons work uniformly.
2830
+ ───────────────────────────────────────────────────────────────────────────── */
2831
+ function vecOf(r){
2832
+ if(!r) return 'NEUTRAL';
2833
+ const fd = r.flip_direction ? String(r.flip_direction).toUpperCase() : '';
2834
+ if(fd === 'BUY' || fd === 'SELL') return fd;
2835
+ const ls = r.latest_signal || r.dominant_signal;
2836
+ const lu = ls ? String(ls).toUpperCase() : '';
2837
+ if(lu === 'BUY' || lu === 'SELL') return lu;
2838
+ return 'NEUTRAL';
2839
+ }
2840
+
2841
  function scoreBar(s) {
2842
  const v=parseFloat(s)||0; const p=Math.min(Math.abs(v)*100,100).toFixed(1);
2843
  const c=v>.05?'#00df8a':v<-.05?'#ff3d5a':'#00c8ff';
 
2858
  return `<tr class="${sel} ${rcls}" onclick="onRow('${r.space_name}',this)">
2859
  <td class="rc">${rk}</td><td class="nc">${r.space_name}</td><td>${scoreBar(r.score)}</td>
2860
  <td class="num">${fPct(r.signal_confidence)}</td><td class="num">${fPct(r.avn_accuracy)}</td>
2861
+ <td><span class="sig ${vecOf(r)}">${vecOf(r)}</span></td>
2862
  <td class="num">${(r.training_steps||0).toLocaleString()}</td><td class="num">${f4(r.actor_loss)}</td>
2863
  <td>${stxt(r)}</td></tr>`;
2864
  }).join('');
 
2886
  <div class="dr"><span class="dk">Model Core</span><span class="dv">QUASAR-X1</span></div>
2887
  </div>
2888
  <div><div class="dc-title">Ensemble Consensus</div>
2889
+ <div class="dr"><span class="dk">Inference Signal</span><span class="dv ${(vecOf(r))==='BUY'?'g':(vecOf(r))==='SELL'?'r':''}">${vecOf(r)}</span></div>
2890
  <div class="dr"><span class="dk">Buy Votes</span><span class="dv g">${r.buy_count}</span></div>
2891
  <div class="dr"><span class="dk">Sell Votes</span><span class="dv r">${r.sell_count}</span></div>
2892
  </div>`;
 
2896
  function updateCharts(){
2897
  scoreChart.data.labels=_rankings.map(r=>r.space_name);
2898
  scoreChart.data.datasets[0].data=_rankings.map(r=>r.score);
2899
+ scoreChart.data.datasets[0].backgroundColor=_rankings.map(r=>{ const sig=vecOf(r); return sig==='BUY'?'rgba(0,223,138,0.7)': sig==='SELL'?'rgba(255,61,90,0.7)':'rgba(0,200,255,0.5)'; });
2900
  scoreChart.update();
2901
  if(!_rankings.length)return; const top=_rankings[0].space_name; const h=(_hist[top]||[]).slice(-60); if(!h.length)return;
2902
  const lb=h.map(p=>new Date(p.ts*1000).toLocaleTimeString('en-GB',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}));
 
3285
 
3286
  // Derive per-asset gate status from ranking data
3287
  function gateStatus(r) {
3288
+ const A = (vecOf(r)) === 'BUY' || (vecOf(r)) === 'SELL';
3289
  const B = r.signal_confidence >= 0.55;
3290
  const C = Math.abs(r.score) >= 0.10;
3291
  const D = r.avn_accuracy >= 0.50;
 
3302
  if (!_rankings || !_rankings.length) return;
3303
 
3304
  // ── KPI Row ────────────────────────────────────────────────────────────────
3305
+ const buys = _rankings.filter(r => (vecOf(r)) === 'BUY');
3306
+ const sells = _rankings.filter(r => (vecOf(r)) === 'SELL');
3307
  document.getElementById('t-kpi-buys').textContent = buys.length;
3308
  document.getElementById('t-kpi-sells').textContent = sells.length;
3309
 
 
3332
  const allPass = allGatesPass(gates);
3333
  const gateIcon = allPass ? '<span style="color:var(--green);font-weight:800">βœ“ CLEAR</span>'
3334
  : '<span style="color:var(--amber);font-weight:700">⚠ GATED</span>';
3335
+ const sigClass = (vecOf(r)) === 'BUY' ? 'BUY' : (vecOf(r)) === 'SELL' ? 'SELL' : 'HOLD';
3336
  return `<tr>
3337
  <td class="rc ${i===0?'r1':''}">${r.rank}</td>
3338
  <td class="nc">${r.space_name}</td>
3339
+ <td><span class="sig ${sigClass}">${vecOf(r)}</span></td>
3340
  <td class="num">${fPct(r.signal_confidence)}</td>
3341
  <td class="num" style="color:var(--green)">${r.buy_count}</td>
3342
  <td class="num" style="color:var(--red)">${r.sell_count}</td>
 
3366
  const gLabel = allP
3367
  ? '<span class="pos-gate-pass">βœ“ ALL CLEAR</span>'
3368
  : `<span class="pos-gate-warn">A:${gates.A?'βœ“':'βœ—'} B:${gates.B?'βœ“':'βœ—'} C:${gates.C?'βœ“':'βœ—'} D:${gates.D?'βœ“':'βœ—'} E:${gates.E?'βœ“':'βœ—'}</span>`;
3369
+ const side = vecOf(r) === 'BUY' ? 'BUY' : 'SELL';
3370
  return `<tr>
3371
  <td class="nc">${r.space_name}</td>
3372
  <td><span class="sig ${side}">${side}</span></td>
 
3384
  const total = r.buy_count + r.sell_count || 1;
3385
  const buyPct = (r.buy_count / total * 100).toFixed(0);
3386
  const sellPct = (r.sell_count / total * 100).toFixed(0);
3387
+ const sigCls = (vecOf(r)) === 'BUY' ? 'BUY' : (vecOf(r)) === 'SELL' ? 'SELL' : 'HOLD';
3388
  const tagBg = sigCls === 'BUY' ? 'rgba(0,223,138,0.12)' : sigCls === 'SELL' ? 'rgba(255,61,90,0.12)' : 'rgba(255,255,255,0.07)';
3389
  const tagClr = sigCls === 'BUY' ? 'var(--green)' : sigCls === 'SELL' ? 'var(--red)' : 'var(--t2)';
3390
  return `<div class="vote-row">
3391
  <div class="vote-asset-label">
3392
  <span class="vote-asset-name">${r.space_name}</span>
3393
+ <span class="vote-signal-tag" style="background:${tagBg};color:${tagClr};border:1px solid ${tagClr}40">${vecOf(r)}</span>
3394
  </div>
3395
  <div class="vote-bar-track">
3396
  <div class="vote-bar-buy" style="width:${buyPct}%"></div>
 
3428
  const fb = document.getElementById('trade-feed-body');
3429
  const now = Date.now();
3430
  fb.innerHTML = _rankings.slice(0, 20).map(r => {
3431
+ const dir = vecOf(r) === 'BUY' ? 'L' : vecOf(r) === 'SELL' ? 'S' : 'N';
3432
+ const dirLabel = vecOf(r) === 'BUY' ? 'LONG' : vecOf(r) === 'SELL' ? 'SHORT' : 'HOLD';
3433
  const ts = r.last_updated ? new Date(r.last_updated * 1000).toLocaleTimeString('en-GB', {hour12:false}) : 'β€”';
3434
  return `<div class="feed-cols row">
3435
  <div class="f-info">
 
3516
  // ── Summary bar ───────────────────────────────────────────────────────────────
3517
  function renderAssetSummary(assets) {
3518
  document.getElementById('as-total').textContent = assets.length;
3519
+ document.getElementById('as-bull').textContent = assets.filter(r => (vecOf(r)) === 'BUY').length;
3520
+ document.getElementById('as-bear').textContent = assets.filter(r => (vecOf(r)) === 'SELL').length;
3521
+ document.getElementById('as-neut').textContent = assets.filter(r => (vecOf(r)) !== 'BUY' && (vecOf(r)) !== 'SELL').length;
3522
  const accs = assets.filter(r => r.avn_accuracy > 0).map(r => r.avn_accuracy);
3523
  document.getElementById('as-avgacc').textContent = accs.length
3524
  ? fPct(accs.reduce((a,b) => a+b, 0) / accs.length) : 'β€”';
 
3533
  return;
3534
  }
3535
  grid.innerHTML = assets.map(r => {
3536
+ const sigCls = vecOf(r) === 'BUY' ? 'sig-buy' : vecOf(r) === 'SELL' ? 'sig-sell' : 'sig-hold';
3537
  const scoreClr = r.score > 0.1 ? 'var(--green)' : r.score < -0.05 ? 'var(--red)' : 'var(--cyan)';
3538
  const total = r.buy_count + r.sell_count || 1;
3539
  const buyPct = (r.buy_count / total * 100).toFixed(0);
 
3545
  <div class="ac-name">${r.space_name}</div>
3546
  <div class="ac-rank">RANK #${r.rank}</div>
3547
  </div>
3548
+ <span class="sig ${vecOf(r) === 'BUY' ? 'BUY' : vecOf(r) === 'SELL' ? 'SELL' : 'HOLD'}">${vecOf(r)}</span>
3549
  </div>
3550
  <div class="ac-score-block">
3551
  <div class="ac-score-label">AXRVI Score</div>
 
3590
  return;
3591
  }
3592
  body.innerHTML = assets.map(r => {
3593
+ const sigCls = (vecOf(r)) === 'BUY' ? 'BUY' : (vecOf(r)) === 'SELL' ? 'SELL' : 'HOLD';
3594
  const lossClr = r.actor_loss < 0.3 ? 'var(--green)' : r.actor_loss > 0.7 ? 'var(--red)' : 'var(--amber)';
3595
  return `<tr onclick="openDeepDive('${r.space_name}')" style="cursor:pointer">
3596
  <td class="rc ${r.rank===1?'r1':''}">${r.rank}</td>
3597
  <td class="nc">${r.space_name}</td>
3598
+ <td style="text-align:right"><span class="sig ${sigCls}">${vecOf(r)}</span></td>
3599
  <td class="num" style="color:var(--cyan)">${fPct(r.signal_confidence)}</td>
3600
  <td class="num">${fPct(r.avn_accuracy)}</td>
3601
  <td class="num" style="color:${r.score>0.1?'var(--green)':r.score<0?'var(--red)':'var(--t1)'}">${f4(r.score)}</td>
 
3668
  dive.style.display = '';
3669
  dive.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
3670
 
3671
+ const sigCls = vecOf(r) === 'BUY' ? 'g' : vecOf(r) === 'SELL' ? 'r' : '';
3672
  document.getElementById('dive-stats').innerHTML = `
3673
  <div class="dive-block">
3674
  <div class="dive-block-title">Neural Output</div>
3675
  <div class="dive-row"><span class="dive-k">AXRVI Score</span><span class="dive-v ${r.score > 0.1 ? 'g' : r.score < 0 ? 'r' : 'c'}">${f4(r.score)}</span></div>
3676
  <div class="dive-row"><span class="dive-k">Signal Confidence</span><span class="dive-v c">${fPct(r.signal_confidence)}</span></div>
3677
+ <div class="dive-row"><span class="dive-k">Dominant Signal</span><span class="dive-v ${sigCls}">${vecOf(r)}</span></div>
3678
  <div class="dive-row"><span class="dive-k">Rank</span><span class="dive-v">#${r.rank}</span></div>
3679
  <div class="dive-row"><span class="dive-k">Last Updated</span><span class="dive-v">${ago(r.last_updated)}</span></div>
3680
  </div>