[object Object]

← back to Stock Vshape Viewer

Score transparency: expose scoreParts from the engine + render per-stock breakdown chips (fit/dip/RSI/entry/vol) — answers the DTD 'arbitrary weights' critique by making the score auditable

ef0e646890fd77be28d7ebf7d3ca7bccb13bba8b · 2026-08-28 00:19:34 -0700 · Steve

Files touched

Diff

commit ef0e646890fd77be28d7ebf7d3ca7bccb13bba8b
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 28 00:19:34 2026 -0700

    Score transparency: expose scoreParts from the engine + render per-stock breakdown chips (fit/dip/RSI/entry/vol) — answers the DTD 'arbitrary weights' critique by making the score auditable
---
 public/app.js     |  3 +++
 public/index.html |  5 +++++
 public/screen.js  | 14 +++++++-------
 3 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/public/app.js b/public/app.js
index 85cf16e..5d51940 100644
--- a/public/app.js
+++ b/public/app.js
@@ -320,6 +320,9 @@ function renderDetailBody(t, D) {
     <div class="kpi newhigh"><span>recent high</span><b>$${an.rh.price.toFixed(2)}</b>${an.rh.date.slice(5)}</div>
     <div class="kpi"><span>vs 50-DMA</span><b style="color:${an.d50>=0?'var(--pass)':'var(--fail)'}">${an.d50==null?'—':(an.d50>=0?'+':'')+an.d50.toFixed(1)+'%'}</b></div>
     <div class="kpi"><span>vs 200-DMA</span><b style="color:${an.d200>=0?'var(--pass)':'var(--fail)'}">${an.d200==null?'—':(an.d200>=0?'+':'')+an.d200.toFixed(1)+'%'}</b></div>`;
+  document.getElementById('d-scoreparts').innerHTML = (an.scoreParts && an.scoreParts.length)
+    ? `<span>setup score ${an.buyScore} =</span>` + an.scoreParts.map(p => `<span class="sp ${p.v >= 0 ? 'pos' : 'neg'}">${p.k} ${p.v >= 0 ? '+' : ''}${p.v}</span>`).join('')
+    : '<span>setup score 0 — no qualifying components</span>';
   document.getElementById('d-vnote').textContent = D.custom
     ? `Custom range ${D.days[0].date} → ${D.days[D.days.length-1].date} (${D.days.length} sessions) — anchors, MAs, RSI & buy-score recomputed for this range.`
     : `Live daily prices. MAs over full history, sliced to the ${TF.find(x=>x.n===state.tfN)?.k||''} window.`;
diff --git a/public/index.html b/public/index.html
index 60e4b1f..aef9f70 100644
--- a/public/index.html
+++ b/public/index.html
@@ -142,6 +142,10 @@
   .kpis{display:flex;gap:12px;flex-wrap:wrap;margin:6px 0}
   .kpi{font-size:11px;color:var(--dim)}.kpi b{display:block;font-size:13.5px;color:var(--ink)}
   .kpi.high b{color:var(--high)}.kpi.trough b{color:var(--trough)}.kpi.newhigh b{color:var(--newhigh)}
+  .scoreparts{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin:2px 0 6px;font-size:10.5px;color:var(--dim)}
+  .scoreparts .sp{padding:2px 7px;border-radius:6px;font-weight:600}
+  .scoreparts .sp.pos{background:rgba(57,217,138,.14);color:var(--pass)}
+  .scoreparts .sp.neg{background:rgba(255,92,122,.14);color:var(--fail)}
   .listtoggle{margin:8px 0 4px;cursor:pointer;color:#8fb2ff;font-size:12px}
   table.daily{width:100%;border-collapse:collapse;font-size:11px;display:none}
   table.daily.show{display:table}
@@ -267,6 +271,7 @@
   <div class="brushwrap"><canvas id="d-brush"></canvas>
     <div class="brushhint" id="d-brushhint">↔ drag across the strip to select a custom range · click a timeframe to reset</div></div>
   <div class="kpis" id="d-kpis"></div>
+  <div class="scoreparts" id="d-scoreparts"></div>
   <div class="listtoggle" id="d-listtoggle">▸ Daily list (entry analysis)</div>
   <table class="daily" id="d-daily"></table>
   <div class="listtoggle" id="d-opttoggle" style="color:#2ee6c8">▸ Cheap far-dated options (puts & calls)</div>
diff --git a/public/screen.js b/public/screen.js
index 585ade5..0efc699 100644
--- a/public/screen.js
+++ b/public/screen.js
@@ -128,12 +128,12 @@
     // DTD 7/8-B rework: dip-depth reward is GATED on newHigh (reward a RECLAIMED deep
     // dip, never a still-falling knife); redundant new-high +8 removed (it duplicated
     // the fits gate and didn't change the ranking of surfaced candidates).
-    let score = 0;
-    if (fits) score += 30;
-    if (newHigh) score += Math.min(20, Math.max(0, Math.abs(drawdown) - f.minDip)); // depth of a RECOVERED dip only
-    if (rsiNow != null) { if (rsiNow >= 40 && rsiNow <= 60) score += 15; else if (rsiNow < 40) score += 12; else if (rsiNow > 70) score -= 10; }
-    if (zoneCls === 'buy2') score += 22; else if (zoneCls === 'buy') score += 18; else if (zoneCls === 'watch') score += 10; else if (zoneCls === 'ext') score -= 6;
-    if (volSurge > 1.3) score += 5;
+    let score = 0; const parts = [];
+    if (fits) { score += 30; parts.push({ k: 'V-shape fit', v: 30 }); }
+    if (newHigh) { const dd = Math.round(Math.min(20, Math.max(0, Math.abs(drawdown) - f.minDip))); if (dd) { score += dd; parts.push({ k: 'dip depth', v: dd }); } }
+    if (rsiNow != null) { let r = 0; if (rsiNow >= 40 && rsiNow <= 60) r = 15; else if (rsiNow < 40) r = 12; else if (rsiNow > 70) r = -10; if (r) { score += r; parts.push({ k: 'RSI zone', v: r }); } }
+    let z = 0; if (zoneCls === 'buy2') z = 22; else if (zoneCls === 'buy') z = 18; else if (zoneCls === 'watch') z = 10; else if (zoneCls === 'ext') z = -6; if (z) { score += z; parts.push({ k: 'entry vs MA', v: z }); }
+    if (volSurge > 1.3) { score += 5; parts.push({ k: 'volume surge', v: 5 }); }
     score = Math.max(0, Math.min(100, Math.round(score)));
 
     let why;
@@ -146,7 +146,7 @@
       eh, tr, rh, recentWin, weeksAgoActual, drawdown, newHigh, dipOK, fits,
       verdict: fits ? 'PASS' : 'FAIL', why, dipTo50, dipTo200, below50, below200,
       C, m50, m200, d50, d200, pctFromHigh, zoneCls, zoneLabel,
-      rsi: rsiNow == null ? null : Math.round(rsiNow), volSurge: +volSurge.toFixed(2), buyScore: score,
+      rsi: rsiNow == null ? null : Math.round(rsiNow), volSurge: +volSurge.toFixed(2), buyScore: score, scoreParts: parts,
       rsiArr, profile,
       poc: profile ? +profile.poc.toFixed(2) : null,
       vaLo: profile ? +profile.vaLo.toFixed(2) : null,

← 340e745 Ship the backtest into the UI: /api/backtest (built at boot,  ·  back to Stock Vshape Viewer  ·  CSV export: download scan matches (ticker/name/score/price/d 5ca5574 →