← 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
M public/app.jsM public/index.htmlM public/screen.js
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 →