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