@@ -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+ }
9791034function 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 ; }
9801035function 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