Skip to content

Commit a797c49

Browse files
lanfuliclaude
andcommitted
analytics v2 Stage 4: benchmark overlay + alpha + market-adjusted stats
computeMarketStats (build-time, deep 2y returns vs SPY/SMH): beta·SPY, beta·SMH, annualized alpha, 63D relative strength, and a MARKET-ADJUSTED CCF — mentions vs idiosyncratic (residual) returns r_i-(α+β·r_SPY), answering "does she predict alpha, or just ride the sector?". Verified in isolation: pure 2×SPY -> beta=2.000, alpha≈0, no residual signal; idio mention shock -> adjusted CCF peaks lag0 r=0.99 significant. UI: dashed SPY overlay on the focus chart (normalized to entry, y-range expanded so it never clips; legend chip); statsPanel now shows β·SPY/β·SMH · α(年化) · 63D相对强弱 and the "剔除大盘后(残差)" adjusted readout. statsPanel(model,row); market line shows even when the mention model is null (β/α are mention-independent). Renders gracefully with no overlay/market-line until the price backfill (Yahoo rate-limited at commit). Patch sub() uses function-form replace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 22ce5ac commit a797c49

2 files changed

Lines changed: 112 additions & 11 deletions

File tree

reports/aleabito-60d-dashboard.html

Lines changed: 29 additions & 6 deletions
Large diffs are not rendered by default.

scripts/build-aleabito-dashboard.js

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,14 @@ async function main() {
902902
});
903903
// (deepByTicker + benchDeep held in scope for Stage 2/4/5 build-time metrics)
904904
const trackRecordAgg = computeTrackRecord(summary, deepCache, meaningfulSet);
905+
const spyDeepDaily = (deepCache.series.SPY && deepCache.series.SPY.daily) || null;
906+
const smhDeepDaily = (deepCache.series.SMH && deepCache.series.SMH.daily) || null;
907+
summary.forEach((r) => {
908+
if (!meaningfulSet.has(r.ticker)) return;
909+
const daily = deepByTicker.get(r.ticker); if (!daily) return;
910+
const mbd = {}; (r.mentionSeries || []).forEach((p) => { mbd[p.date] = (mbd[p.date] || 0) + (p.mentioned_posts || 0); });
911+
r.marketStats = computeMarketStats(daily, spyDeepDaily, smhDeepDaily, mbd);
912+
});
905913

906914
const themeStats = new Map();
907915
summary.forEach((row) => {
@@ -976,6 +984,53 @@ async function main() {
976984
}, null, 2));
977985
}
978986

987+
function pearsonB(a, b) {
988+
const n = Math.min(a.length, b.length); if (n < 5) return 0;
989+
let sa = 0, sb = 0; for (let i = 0; i < n; i++) { sa += a[i]; sb += b[i]; }
990+
const ma = sa / n, mb = sb / n; let num = 0, da = 0, db = 0;
991+
for (let i = 0; i < n; i++) { const xa = a[i] - ma, xb = b[i] - mb; num += xa * xb; da += xa * xa; db += xb * xb; }
992+
if (da === 0 || db === 0) return 0; return num / Math.sqrt(da * db);
993+
}
994+
function logRetSeries(daily) {
995+
const out = [];
996+
for (let i = 1; i < daily.length; i++) { if (daily[i - 1].c > 0 && daily[i].c > 0) out.push({ d: daily[i].d, r: Math.log(daily[i].c / daily[i - 1].c) }); }
997+
return out;
998+
}
999+
function computeMarketStats(daily, spyDaily, smhDaily, mentionByDate) {
1000+
if (!daily || daily.length < 30 || !spyDaily || !spyDaily.length) return null;
1001+
const tr = logRetSeries(daily);
1002+
const spyMap = new Map(); for (const x of logRetSeries(spyDaily)) spyMap.set(x.d, x.r);
1003+
const smhMap = new Map(); if (smhDaily) for (const x of logRetSeries(smhDaily)) smhMap.set(x.d, x.r);
1004+
const dts = [], ri = [], rspy = [], rsmh = [];
1005+
for (const x of tr) { const s = spyMap.get(x.d); if (s != null) { dts.push(x.d); ri.push(x.r); rspy.push(s); rsmh.push(smhMap.has(x.d) ? smhMap.get(x.d) : null); } }
1006+
const n = ri.length; if (n < 20) return null;
1007+
const mean = (a) => a.reduce((s, x) => s + x, 0) / a.length;
1008+
const mi = mean(ri), msp = mean(rspy);
1009+
let cov = 0, varS = 0; for (let i = 0; i < n; i++) { cov += (ri[i] - mi) * (rspy[i] - msp); varS += (rspy[i] - msp) * (rspy[i] - msp); }
1010+
if (varS <= 0) return null;
1011+
const beta = cov / varS, alpha = mi - beta * msp, alphaAnnual = alpha * 252;
1012+
let betaSMH = null;
1013+
if (rsmh.every((x) => x != null)) {
1014+
const msmh = mean(rsmh); let covS = 0, vS = 0; for (let i = 0; i < n; i++) { covS += (ri[i] - mi) * (rsmh[i] - msmh); vS += (rsmh[i] - msmh) * (rsmh[i] - msmh); }
1015+
if (vS > 0) betaSMH = covS / vS;
1016+
}
1017+
const tail = Math.min(63, n); let cumI = 0, cumS = 0; for (let i = n - tail; i < n; i++) { cumI += ri[i]; cumS += rspy[i]; }
1018+
const relStr = (Math.exp(cumI) - 1) - (Math.exp(cumS) - 1);
1019+
const resid = ri.map((r, i) => r - (alpha + beta * rspy[i]));
1020+
const m = dts.map((d) => (mentionByDate && mentionByDate[d]) || 0);
1021+
let adjContemp = null, adjBest = null, adjSig = null, adjN = 0;
1022+
if (m.reduce((s, x) => s + x, 0) > 0) {
1023+
adjN = n; adjSig = 1.96 / Math.sqrt(n); const ccf = [];
1024+
for (let k = -7; k <= 7; k++) {
1025+
const xs = [], ys = [];
1026+
for (let t = 0; t < n; t++) { const tk = t + k; if (tk >= 0 && tk < n) { xs.push(m[t]); ys.push(resid[tk]); } }
1027+
const rr = xs.length >= 5 ? pearsonB(xs, ys) : 0;
1028+
ccf.push({ lag: k, r: rr, sig: Math.abs(rr) > adjSig });
1029+
}
1030+
adjContemp = ccf[7].r; adjBest = ccf[0]; for (const c of ccf) if (Math.abs(c.r) > Math.abs(adjBest.r)) adjBest = c;
1031+
}
1032+
return { beta, alpha, alphaAnnual, betaSMH, relStr, n, adjContemp, adjBest, adjSig, adjN };
1033+
}
9791034
function median(arr) { if (!arr.length) return null; const s = arr.slice().sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; }
9801035
function benchCloseOn(daily, date) {
9811036
let lo = 0, hi = daily.length - 1, ans = -1;
@@ -1802,6 +1857,10 @@ function buildHtmlV2(data) {
18021857
.focus-px { font-family: var(--mono); font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }
18031858
.focus-chg { font-size: 14px; }
18041859
.combo-chart--lg { height: 360px; }
1860+
.combo-bench { fill: none; stroke: var(--purple); stroke-width: 1.5; stroke-dasharray: 4 3; opacity: 0.7; vector-effect: non-scaling-stroke; }
1861+
.combo-legend { font-size: 10px; color: var(--purple); margin-left: 8px; font-weight: 700; }
1862+
.stats-market { font-family: var(--mono); font-size: 11.5px; color: var(--ink); margin-top: 8px; line-height: 1.6; }
1863+
.stats-adj { font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.6; }
18051864
.stats-panel { margin-top: 14px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 14px; background: rgba(13,18,33,0.55); }
18061865
.stats-verdict { display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 700; color: var(--ink); }
18071866
.stats-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }
@@ -3373,7 +3432,15 @@ function buildHtmlV2(data) {
33733432
const mMap = {};
33743433
for (const p of (row.mentionSeries || [])) mMap[p.date] = (mMap[p.date] || 0) + (p.mentioned_posts || 0);
33753434
const ment = dates.map((d) => mMap[d] || 0);
3376-
const pMin = Math.min.apply(null, price), pMax = Math.max.apply(null, price), pSpan = (pMax - pMin) || 1;
3435+
var bench = null, hasBench = false;
3436+
var BM = DASHBOARD_DATA.benchmarks;
3437+
if (BM && BM.SPY && BM.SPY.points && BM.SPY.points.length) {
3438+
var bmap = {}; BM.SPY.points.forEach(function (bp) { bmap[bp.date] = bp.close; });
3439+
var bbase = null; for (var bi = 0; bi < dates.length; bi++) { if (bmap[dates[bi]] != null) { bbase = bmap[dates[bi]]; break; } }
3440+
if (bbase && price[0] > 0) { bench = dates.map(function (d) { var c = bmap[d]; return c != null ? price[0] * (c / bbase) : null; }); hasBench = bench.some(function (v) { return v != null; }); }
3441+
}
3442+
var allV = price.slice(); if (hasBench) bench.forEach(function (v) { if (v != null) allV.push(v); });
3443+
const pMin = Math.min.apply(null, allV), pMax = Math.max.apply(null, allV), pSpan = (pMax - pMin) || 1;
33773444
const mMax = Math.max.apply(null, ment) || 1;
33783445
const n = pts.length, stepX = (W - padX * 2) / Math.max(n - 1, 1);
33793446
const yOf = (v) => padTop + (1 - (v - pMin) / pSpan) * (H - padTop - padBot);
@@ -3383,6 +3450,8 @@ function buildHtmlV2(data) {
33833450
const fid = 'cg' + (++__comboSeq);
33843451
const line = smoothPath(coords);
33853452
const area = line + ' L' + coords[n - 1][0].toFixed(1) + ' ' + (H - padBot) + ' L' + coords[0][0].toFixed(1) + ' ' + (H - padBot) + ' Z';
3453+
var benchPath = '';
3454+
if (hasBench) { var bcoords = []; for (var bj = 0; bj < bench.length; bj++) if (bench[bj] != null) bcoords.push([padX + bj * stepX, yOf(bench[bj])]); if (bcoords.length >= 2) benchPath = '<path class="combo-bench" d="' + smoothPath(bcoords) + '"></path>'; }
33863455
const barW = Math.max(1.2, Math.min(7, stepX * 0.55)), barMaxH = (H - padTop - padBot) * 0.42;
33873456
let bars = '';
33883457
for (let i = 0; i < n; i++) {
@@ -3391,12 +3460,13 @@ function buildHtmlV2(data) {
33913460
bars += '<rect class="combo-bar' + (ment[i] >= 0.7 * mMax ? ' spike' : '') + '" x="' + bx.toFixed(1) + '" y="' + by.toFixed(1) + '" width="' + barW.toFixed(1) + '" height="' + bh.toFixed(1) + '" rx="1"></rect>';
33923461
}
33933462
return '<div class="combo-wrap">' +
3394-
'<div class="combo-hdr"><span>价格走势 · 柱 = 当日提及量</span><span class="combo-hdr-px" style="color:' + stroke + '">' + formatNumber(price[n - 1], 2) + ' ' + html(row.price.currency || '') + ' · ' + formatPct(row.price.change_pct) + '</span></div>' +
3463+
'<div class="combo-hdr"><span>价格走势 · 柱 = 当日提及量' + (hasBench ? ' <span class="combo-legend">— SPY</span>' : '') + '</span><span class="combo-hdr-px" style="color:' + stroke + '">' + formatNumber(price[n - 1], 2) + ' ' + html(row.price.currency || '') + ' · ' + formatPct(row.price.change_pct) + '</span></div>' +
33953464
'<svg class="combo-chart js-combo-chart' + (big ? ' combo-chart--lg' : '') + '" data-ticker="' + row.ticker + '" data-h="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="none" role="img" aria-label="' + row.ticker + ' 价格与提及叠加图">' +
33963465
'<defs><linearGradient id="' + fid + '" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="' + stroke + '" stop-opacity="0.32"></stop><stop offset="82%" stop-color="' + stroke + '" stop-opacity="0.02"></stop></linearGradient></defs>' +
33973466
'<line class="combo-base" x1="' + padX + '" y1="' + (H - padBot) + '" x2="' + (W - padX) + '" y2="' + (H - padBot) + '"></line>' +
33983467
bars +
33993468
'<path d="' + area + '" fill="url(#' + fid + ')"></path>' +
3469+
benchPath +
34003470
'<path class="combo-line" d="' + line + '" stroke="' + stroke + '"></path>' +
34013471
'<g class="combo-cross"><line class="cc-line" y1="' + padTop + '" y2="' + (H - padBot) + '" stroke="#edf3ff" stroke-width="1" opacity="0.5"></line><circle class="cc-dot" r="3.5" fill="#edf3ff"></circle></g>' +
34023472
'</svg>' +
@@ -3560,8 +3630,15 @@ function buildHtmlV2(data) {
35603630
bars +
35613631
'</svg>';
35623632
}
3563-
function statsPanel(model) {
3564-
if (!model) return '<div class="stats-panel stats-na">提及 × 股价模型:重叠的价格/提及数据不足(需 ≥12 个交易日),暂无法建模。</div>';
3633+
function statsPanel(model, row) {
3634+
var ms = row && row.marketStats;
3635+
var fr2 = function (v) { return v == null || !isFinite(v) ? '—' : (v >= 0 ? '+' : '') + v.toFixed(2); };
3636+
var marketLine = '';
3637+
if (ms) {
3638+
marketLine = '<div class="stats-market">β·SPY=' + fr2(ms.beta) + (ms.betaSMH != null ? ' · β·SMH=' + fr2(ms.betaSMH) : '') + ' · α(年化)=' + (ms.alphaAnnual == null ? '—' : formatPct(ms.alphaAnnual * 100)) + ' · 63D相对强弱=' + (ms.relStr == null ? '—' : formatPct(ms.relStr * 100)) + '</div>';
3639+
if (ms.adjBest) marketLine += '<div class="stats-adj">剔除大盘后(残差·完整窗口):同步 ρ=' + fr2(ms.adjContemp) + ' · 峰值 lag=' + (ms.adjBest.lag > 0 ? '+' : '') + ms.adjBest.lag + ' r=' + fr2(ms.adjBest.r) + (ms.adjBest.sig ? ' ✓ 仍显著' : ' · 噪声内') + '</div>';
3640+
}
3641+
if (!model) return '<div class="stats-panel">' + marketLine + '<div class="stats-na">提及 × 股价模型:重叠的价格/提及数据不足(需 ≥12 个交易日),暂无法建模。</div></div>';
35653642
const fr = (v) => (v >= 0 ? '+' : '') + v.toFixed(2);
35663643
const fp = (v) => v < 0.001 ? '<0.001' : v.toFixed(3);
35673644
const gf = model.grangerFwd, gr = model.grangerRev;
@@ -3580,6 +3657,7 @@ function buildHtmlV2(data) {
35803657
' · CCF 峰值 lag=' + (best.lag > 0 ? '+' : '') + best.lag + ' r=' + fr(best.r) + (best.sig ? ' ✓' : ' (ns)');
35813658
return '<div class="stats-panel ' + vcls + '">' +
35823659
'<div class="stats-verdict"><span class="stats-dot"></span>' + verdict + '</div>' +
3660+
marketLine +
35833661
'<div class="stats-ccf"><div class="stats-ccf-cap"><span>互相关 CCF:提及 → 收益(滞后/领先天数)</span><span class="muted">虚线=95%显著带 ±' + model.sig.toFixed(2) + '</span></div>' + ccfChart(model) +
35843662
'<div class="ccf-axis"><span>← 收益领先提及(她在反应)</span><span>提及领先收益(她在预测)→</span></div></div>' +
35853663
'<div class="stats-tech">' + tech + '</div>' +
@@ -3658,7 +3736,7 @@ function buildHtmlV2(data) {
36583736
pxHtml + '</div>' +
36593737
trackBadge(row) +
36603738
combinedChart(row, true) +
3661-
statsPanel(model);
3739+
statsPanel(model, row);
36623740
attachComboHandlers();
36633741
}
36643742
function initGridSplitter() {

0 commit comments

Comments
 (0)