[object Object]

← back to Stock Vshape Viewer

yoloforever cycle 2: buy-score is CAP-DEPENDENT. Backtest now SEGMENTED by cap tier (large 120d +0.68 works / small -0.6 inverts); backtest panel shows both; per-stock cap-tier flag (large=sweet-spot, small<20B=INVERTS warning, mid 20-40B=untested); /api/cap resolves cap for manual adds

7a16bf97f100c8f7bdc182b3b582dd9f2543a35d · 2026-08-28 01:02:00 -0700 · Steve

Files touched

Diff

commit 7a16bf97f100c8f7bdc182b3b582dd9f2543a35d
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 28 01:02:00 2026 -0700

    yoloforever cycle 2: buy-score is CAP-DEPENDENT. Backtest now SEGMENTED by cap tier (large 120d +0.68 works / small -0.6 inverts); backtest panel shows both; per-stock cap-tier flag (large=sweet-spot, small<20B=INVERTS warning, mid 20-40B=untested); /api/cap resolves cap for manual adds
---
 public/app.js | 62 ++++++++++++++++++++++++++++++++++++++++++-----------------
 server.js     | 54 ++++++++++++++++++++++++++++++---------------------
 2 files changed, 76 insertions(+), 40 deletions(-)

diff --git a/public/app.js b/public/app.js
index 6711a90..cae65bb 100644
--- a/public/app.js
+++ b/public/app.js
@@ -20,6 +20,7 @@ const state = {
   volLayer: load(LS.volLayer, true),
   volNorm: load(LS.volNorm, 'per-stock'),
   meta: Object.fromEntries(window.SEED.map(s => [s.ticker, s])),
+  caps: {},       // ticker -> market cap ($) when known (from scan matches)
   raw: {},        // ticker -> { days:[...full], source, asOf } | { error }
   data: {},       // ticker -> processed { days, ma50, ma200, an, min, max }
   selected: null,
@@ -27,6 +28,12 @@ const state = {
 function load(k, d) { try { const v = JSON.parse(localStorage.getItem(k)); return v ?? d; } catch { return d; } }
 function save(k, v) { localStorage.setItem(k, JSON.stringify(v)); }
 function metaOf(t) { return state.meta[t] || { ticker: t, name: t, cls: '—', marketCap: '—', sector: '—' }; }
+// resolve a ticker's market cap ($) from scan data, else parse the seed metadata string
+function capOf(t) {
+  if (state.caps && state.caps[t] != null) return state.caps[t];
+  const m = String(metaOf(t).marketCap || '').replace(/[~$,\s]/g, '').match(/([\d.]+)([TBM])/i);
+  return m ? parseFloat(m[1]) * ({ T: 1e12, B: 1e9, M: 1e6 })[m[2].toUpperCase()] : null;
+}
 
 // ============================ three.js scene ============================
 const app = document.getElementById('app');
@@ -342,12 +349,21 @@ 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>`;
-  const sweet = an.buyScore >= 40 && an.buyScore <= 79
-    ? ` <span class="sp pos" title="Backtest: the 40–79 band beat baseline at every horizon; 80+ underperformed. Higher isn't always better.">★ backtest sweet spot</span>`
-    : (an.buyScore >= 80 ? ` <span class="sp neg" title="Backtest: the 80+ band was rare and underperformed the 40–79 sweet spot.">⚠ above sweet spot</span>` : '');
+  // three validated cap tiers: large (≥$40B, score works) · small (<$20B, score INVERTS) · mid ($20–40B, untested)
+  const cap = capOf(t), tier = cap == null ? null : cap < 20e9 ? 'small' : cap < 40e9 ? 'mid' : 'large';
+  const capFlag = tier === 'small'
+    ? ` <span class="sp neg" title="Backtest: the buy-score INVERTS on small/mid-caps (120d correlation negative) — a high score here predicted WORSE returns. Validated only on large-caps.">⚠ small/mid-cap · score INVERTS</span>`
+    : tier === 'mid'
+    ? ` <span class="sp" style="background:rgba(255,213,74,.14);color:var(--watch)" title="$20–40B is between the validated large-cap tier (score works) and the small-cap tier (inverts) — treat the score as untested here.">ⓘ cap untested — validated on large-caps only</span>`
+    : '';
+  // only claim the sweet spot on the VALIDATED large-cap tier
+  const sweet = tier === 'large' && an.buyScore >= 40 && an.buyScore <= 79
+    ? ` <span class="sp pos" title="Backtest (large-cap): the 40–79 band beat baseline at every horizon; 80+ underperformed. Higher isn't always better.">★ backtest sweet spot</span>`
+    : (tier === 'large' && an.buyScore >= 80 ? ` <span class="sp neg" title="Backtest: the 80+ band was rare and underperformed the 40–79 sweet spot.">⚠ above sweet spot</span>` : '');
+  const smallWarn = capFlag;
   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>') + sweet;
+    : '<span>setup score 0 — no qualifying components</span>') + sweet + smallWarn;
   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.`;
@@ -367,6 +383,12 @@ function showDetail(t) {
   brush.si = Math.max(0, full.length - D.days.length); brush.ei = full.length - 1;
   drawBrush();
   renderDetailBody(t, D);
+  // resolve cap for a manually-added ticker so the small-cap warning can fire
+  if (capOf(t) == null) {
+    fetch(`/api/cap?ticker=${encodeURIComponent(t)}`).then(r => r.json()).then(j => {
+      if (j && j.cap != null) { state.caps[t] = j.cap; if (state.selected === t && state.detailD) renderDetailBody(t, state.detailD); }
+    }).catch(() => {});
+  }
   highlightSelection();
 }
 function drawBrush() {
@@ -636,6 +658,7 @@ function pollScan(job) {
 function renderResults(matches) {
   const el = document.getElementById('scan-results');
   el.innerHTML = matches.slice(0, 60).map(m => {
+    state.caps[m.ticker] = m.cap; // remember cap for the small-cap warning
     const bcol = m.buyScore >= 60 ? 'var(--pass)' : m.buyScore >= 40 ? 'var(--watch)' : '#63769c';
     return `<div class="r" data-t="${m.ticker}">
       <span class="bs" style="background:rgba(87,140,255,.12);color:${bcol}">${m.buyScore}</span>
@@ -721,24 +744,27 @@ async function loadBacktest() {
 function renderBacktest(j) {
   const box = document.getElementById('d-backtest');
   const pct = x => (x >= 0 ? '+' : '') + (x * 100).toFixed(1) + '%';
+  const cc = v => v >= 0.4 ? 'up' : v <= -0.4 ? 'down' : '';
+  const L = j.large, S = j.small;
+  // headline: score→return correlation BY CAP TIER
+  let html = `<div style="color:#6f83a8;margin:2px 0 4px">does a higher score → higher forward return? (correlation by market-cap tier)</div>`;
+  html += `<table class="daily show" style="font-size:11px"><tr><th>cap tier</th>${L.horizons.map(h => `<th>${h.H}d</th>`).join('')}</tr>`;
+  html += `<tr><td>large-cap (${L.tickers})</td>${L.horizons.map(h => `<td class="${cc(h.corr)}">${h.corr}</td>`).join('')}</tr>`;
+  html += `<tr><td>small-cap (${S.tickers})</td>${S.horizons.map(h => `<td class="${cc(h.corr)}">${h.corr}</td>`).join('')}</tr></table>`;
+  // large-cap band detail (where the signal works)
   const BANDS = [[0, 19], [20, 39], [40, 59], [60, 79], [80, 100]];
-  let html = `<div style="color:#6f83a8;margin:2px 0 4px">${j.tickers} names · ${j.sampleDays} sample days · mean forward return by score band</div>`;
-  html += `<table class="daily show" style="font-size:11px"><tr><th>score band</th>`;
-  j.horizons.forEach(h => html += `<th>${h.H}d</th>`);
-  html += `</tr><tr><td style="color:var(--dim)">any day (baseline)</td>`;
-  j.horizons.forEach(h => html += `<td style="color:var(--dim)">${pct(h.baseline)}</td>`);
-  html += `</tr>`;
+  html += `<div style="color:#6f83a8;margin:8px 0 3px">large-cap forward return by score band (the tier where it works)</div>`;
+  html += `<table class="daily show" style="font-size:11px"><tr><th>band</th>${L.horizons.map(h => `<th>${h.H}d</th>`).join('')}</tr>`;
+  html += `<tr><td style="color:var(--dim)">baseline</td>${L.horizons.map(h => `<td style="color:var(--dim)">${pct(h.baseline)}</td>`).join('')}</tr>`;
   for (const [lo, hi] of BANDS) {
-    html += `<tr><td>${lo}-${hi}</td>`;
-    j.horizons.forEach(h => { const b = h.bands.find(x => x.lo === lo); if (!b || b.n < 15) { html += `<td style="color:#4a5877">·</td>`; return; }
-      html += `<td class="${b.mret > h.baseline ? 'up' : 'down'}" title="n=${b.n}, win ${(b.win * 100).toFixed(0)}%">${pct(b.mret)}</td>`; });
-    html += `</tr>`;
+    html += `<tr><td>${lo}-${hi}</td>${L.horizons.map(h => { const b = h.bands.find(x => x.lo === lo); if (!b || b.n < 15) return `<td style="color:#4a5877">·</td>`;
+      return `<td class="${b.mret > h.baseline ? 'up' : 'down'}" title="n=${b.n}, win ${(b.win * 100).toFixed(0)}%">${pct(b.mret)}</td>`; }).join('')}</tr>`;
   }
   html += `</table>`;
-  const c = H => (j.horizons.find(h => h.H === H) || {}).corr;
-  html += `<div style="font-size:11px;margin-top:6px;line-height:1.5">
-    <b style="color:var(--newhigh)">Read:</b> score→return correlation is <b class="${c(20) >= 0 ? 'up' : 'down'}">${c(20)}</b> at 20d vs <b class="up">${c(120)}</b> at 120d.
-    The buy-score is a <b>~6-month setup signal</b>, not a short-term timer — a high score means "start a position to hold months," not "it pops next week." (Sample: large-caps + ETFs, recent 2yr; not risk-adjusted.)</div>`;
+  const l120 = (L.horizons.find(h => h.H === 120) || {}).corr, s120 = (S.horizons.find(h => h.H === 120) || {}).corr;
+  html += `<div style="font-size:11px;margin-top:6px;line-height:1.5"><b style="color:var(--newhigh)">Read:</b>
+    the setup score works on <b class="up">large-caps (120d corr ${l120})</b> but <b class="down">INVERTS on speculative small-caps (120d ${s120})</b>.
+    Trust the score on quality large-caps as a ~6-month signal; treat a high score on a small-cap with skepticism. (Recent 2yr; not risk-adjusted.)</div>`;
   box.innerHTML = html;
 }
 
diff --git a/server.js b/server.js
index fd6b64e..59ba81e 100644
--- a/server.js
+++ b/server.js
@@ -98,39 +98,44 @@ async function getOptions(t) {
 
 // ---------------- score backtest (built once at boot, cached) ----------------
 let BT = null, BT_TS = 0, BT_BUILDING = false; const BT_TTL = 6 * 60 * 60 * 1000;
-const BT_SAMPLE = ['NVDA','AAPL','MSFT','AMZN','GOOGL','META','TSLA','AVGO','JPM','BAC','WMT','XOM','CVX','UNH','LLY','JNJ','PG','KO','PEP','HD','MCD','NKE','DIS','NFLX','CRM','ORCL','AMD','INTC','CSCO','QCOM','TXN','IBM','GE','CAT','BA','MMM','HON','UPS','SPY','QQQ','XLK','XLF','XLE','SMH'];
+// backtest is SEGMENTED by market-cap tier — the score works on large-caps but inverts
+// on speculative small-caps (yoloforever cycle 2 finding, Steve chose to surface it).
+const BT_LARGE = ['NVDA','AAPL','MSFT','AMZN','GOOGL','META','TSLA','AVGO','JPM','BAC','WMT','XOM','CVX','UNH','LLY','JNJ','PG','KO','PEP','HD','MCD','NKE','DIS','NFLX','CRM','ORCL','AMD','INTC','CSCO','QCOM','TXN','IBM','GE','CAT','BA','MMM','HON','UPS','SPY','QQQ','IWM','XLK','XLF','XLE','SMH','ARKK'];
+const BT_SMALL = ['SOFI','RIVN','AFRM','DKNG','HOOD','CVNA','CELH','TOST','IOT','CHWY','PATH','RUN','RKLB','IONQ','ACHR','FUBO','PLUG','LCID','RBLX','U'];
 const BT_HORIZONS = [20, 60, 120];
 const BT_BANDS = [[0, 19], [20, 39], [40, 59], [60, 79], [80, 100]];
 async function buildBacktest() {
   if (BT_BUILDING) return; BT_BUILDING = true;
   try {
-    const F = SCREEN.DEFAULTS, TF = 126;
-    const rows = { 20: [], 60: [], 120: [] }; let used = 0;
-    for (const t of BT_SAMPLE) {
-      let full; try { full = (await getHistory(t)).days; } catch { continue; }
-      if (!full || full.length < TF + 130) continue; used++;
-      const closes = full.map(d => d.close);
-      const ma50 = SCREEN.sma(closes, 50), ma200 = SCREEN.sma(closes, 200);
-      for (let tE = TF; tE < full.length - Math.max(...BT_HORIZONS); tE += 3) {
-        const an = SCREEN.analyze(full.slice(tE - TF, tE), ma50.slice(tE - TF, tE), ma200.slice(tE - TF, tE), F);
-        for (const H of BT_HORIZONS) { const fwd = full[tE + H].close / full[tE].close - 1; if (isFinite(fwd)) rows[H].push({ s: an.buyScore, f: fwd }); }
+    const F = SCREEN.DEFAULTS, TF = 126, mean = a => a.reduce((x, y) => x + y, 0) / (a.length || 1);
+    const segLists = { large: BT_LARGE, small: BT_SMALL };
+    const rows = { large: { 20: [], 60: [], 120: [] }, small: { 20: [], 60: [], 120: [] } };
+    const used = { large: 0, small: 0 };
+    for (const seg of ['large', 'small']) {
+      for (const t of segLists[seg]) {
+        let full; try { full = (await getHistory(t)).days; } catch { continue; }
+        if (!full || full.length < TF + 130) continue; used[seg]++;
+        const closes = full.map(d => d.close), ma50 = SCREEN.sma(closes, 50), ma200 = SCREEN.sma(closes, 200);
+        for (let tE = TF; tE < full.length - Math.max(...BT_HORIZONS); tE += 3) {
+          const an = SCREEN.analyze(full.slice(tE - TF, tE), ma50.slice(tE - TF, tE), ma200.slice(tE - TF, tE), F);
+          for (const H of BT_HORIZONS) { const fwd = full[tE + H].close / full[tE].close - 1; if (isFinite(fwd)) rows[seg][H].push({ s: an.buyScore, f: fwd }); }
+        }
       }
     }
-    const mean = a => a.reduce((x, y) => x + y, 0) / (a.length || 1);
-    const horizons = BT_HORIZONS.map(H => {
-      const all = rows[H].map(r => r.f), base = mean(all), baseWin = all.filter(x => x > 0).length / (all.length || 1);
-      const bands = BT_BANDS.map(([lo, hi]) => {
-        const f = rows[H].filter(r => r.s >= lo && r.s <= hi).map(r => r.f);
-        return { lo, hi, n: f.length, mret: f.length ? mean(f) : 0, win: f.length ? f.filter(x => x > 0).length / f.length : 0 };
-      });
-      const pop = bands.filter(b => b.n >= 30);
-      let corr = 0;
+    const segStats = seg => BT_HORIZONS.map(H => {
+      const all = rows[seg][H].map(r => r.f), base = mean(all);
+      const bands = BT_BANDS.map(([lo, hi]) => { const f = rows[seg][H].filter(r => r.s >= lo && r.s <= hi).map(r => r.f);
+        return { lo, hi, n: f.length, mret: f.length ? mean(f) : 0, win: f.length ? f.filter(x => x > 0).length / f.length : 0 }; });
+      const pop = bands.filter(b => b.n >= 30); let corr = 0;
       if (pop.length >= 3) { const xs = pop.map((_, i) => i), ys = pop.map(b => b.mret), mx = mean(xs), my = mean(ys);
         const cov = mean(xs.map((x, i) => (x - mx) * (ys[i] - my))), sx = Math.sqrt(mean(xs.map(x => (x - mx) ** 2))), sy = Math.sqrt(mean(ys.map(y => (y - my) ** 2)));
         corr = sx && sy ? cov / (sx * sy) : 0; }
-      return { H, baseline: base, baselineWin: baseWin, bands, corr: +corr.toFixed(2) };
+      return { H, baseline: base, bands, corr: +corr.toFixed(2) };
     });
-    BT = { asOf: new Date().toISOString(), tickers: used, sampleDays: rows[20].length, horizons }; BT_TS = Date.now();
+    BT = { asOf: new Date().toISOString(),
+      large: { tickers: used.large, days: rows.large[20].length, horizons: segStats('large') },
+      small: { tickers: used.small, days: rows.small[20].length, horizons: segStats('small') } };
+    BT_TS = Date.now();
   } catch (e) { /* leave BT null */ } finally { BT_BUILDING = false; }
 }
 
@@ -200,6 +205,11 @@ const server = http.createServer(async (req, res) => {
         minDays: Math.round((+(P.get('minMonths') || 3)) * 30.4), type: P.get('type') || 'both', underlying: o.underlying });
       return jsonRes(res, 200, { ticker: t, underlying: o.underlying, asOf: o.asOf, total: o.chain.length, count: screened.length, contracts: screened.slice(0, 60) });
     }
+    if (pathname === '/api/cap') {
+      const t = tick(); if (!t) return jsonRes(res, 400, { error: 'ticker required' });
+      const u = (await getUniverse(0)).find(x => x.ticker === t);
+      return jsonRes(res, 200, { ticker: t, cap: u && u.cap !== Infinity ? u.cap : null });
+    }
     if (pathname === '/api/backtest') {
       if (BT && Date.now() - BT_TS < BT_TTL) return jsonRes(res, 200, { status: 'ready', ...BT });
       if (!BT_BUILDING) buildBacktest();

← b40722e yoloforever cycle 1: 3D encodings legend (height/volume/verd  ·  back to Stock Vshape Viewer  ·  auto-data-snapshot: 2026-08-28T01:33:48 (1 data files) — .yo 21c2528 →