diff --git a/site/data/manifest.json b/site/data/manifest.json index 5ea3be5..24bb063 100644 --- a/site/data/manifest.json +++ b/site/data/manifest.json @@ -1,14 +1,14 @@ { "schema_version": 3, - "generated_at_utc": "2026-07-30T23:30:41.698835+00:00", + "generated_at_utc": "2026-07-31T00:06:02.197334+00:00", "latest_decision_date": "2026-07-28", "latest_monitor_date": "2026-07-30", "snapshot_count": 17, "evaluation_count": 14, "dashboard_sha256": "42fd55503f3ab38405707e0a43ee0eee2595399e5ed7b54e61a1a1dd4261f608", "dashboard_bytes": 626100, - "horizon_dashboard_sha256": "6388d818bd0e7a103662231c17259e119126864b8fd318e70c96148ec56495cb", - "horizon_dashboard_bytes": 128840, + "horizon_dashboard_sha256": "2ad4f2a23cf0e36cca8cd793ef7a80f45795989da71f55f29f89afa7f20508aa", + "horizon_dashboard_bytes": 129292, "horizon_forecast_arms": 12, "source_policy": "sanitized_live_forward_outputs_only" } \ No newline at end of file diff --git a/site/horizons.html b/site/horizons.html index 27c24fa..ab9e444 100644 --- a/site/horizons.html +++ b/site/horizons.html @@ -35,7 +35,7 @@
Frozen evidence loaded
-

Prospective strategy evidence

An official close mark appears on the entry day. Complete session counts and the cumulative chart advance only when a new open-to-open interval is observable.

The decision session is not a holding session. A July 29 close signal enters at the July 30 open. Its July 30 close can provide an entry-day mark, but the first complete open-to-open session is available at the July 31 open. Entry-day marks are never mixed with validation estimates or final horizon outcomes.
+

Prospective strategy evidence

An official close mark appears on the entry day. It is shown as one observed partial day; the completed open-to-open counter and cumulative chart advance at the next open.

The decision session is not a holding session. A July 29 close signal enters at the July 30 open. At the July 30 close, the dashboard reports 1/5 observed (partial) and the official entry-day mark, while retaining 0 complete open-to-open sessions. The first complete interval is available at the July 31 open. Entry-day marks are never mixed with validation estimates or final horizon outcomes.

Horizon comparison

Paired Ridge and XGBoost development evidence with each arm’s own purge, feature subset, refit interval, and maturity date.

Selected model specifications

The winning development configuration for each visible arm. Open a model’s explainability panel to inspect genuine SHAP importance when that frozen vintage captured it.

Pipeline and risk controls

Freshness, universe, NLP, tactical routing, survival guardrails, and artifact integrity for the selected decision date.

@@ -46,20 +46,20 @@ const data=JSON.parse(document.getElementById('dashboard-data').textContent),decisionSelect=document.getElementById('decisionSelect'),familyFilter=document.getElementById('familyFilter'),horizonFilter=document.getElementById('horizonFilter'),STATE_KEY='live-forward-horizon-dashboard-v1'; const COLORS={ridge:'#3156d3',xgb_ranker:'#0f9f8f',ridge_h5:'#7290eb',ridge_h10:'#4f70dc',ridge_h20:'#3156d3',xgb_ranker_h5:'#70c8bd',xgb_ranker_h10:'#3eb2a4',xgb_ranker_h20:'#0f9f8f',spy:'#d89614'},names={ridge:'Ridge',xgb_ranker:'XGBoost'},fmtPct=v=>v==null||!Number.isFinite(Number(v))?'n/a':(Number(v)*100).toFixed(2)+'%',fmtNum=(v,d=3)=>v==null||!Number.isFinite(Number(v))?'n/a':Number(v).toFixed(d),human=v=>String(v??'').replaceAll('_',' ').replace(/\b\w/g,c=>c.toUpperCase()),tone=v=>Number(v)>=0?'pos':'neg',armColor=a=>COLORS[`${a.family}_h${a.horizon_sessions}`]||COLORS[a.family],armLabel=a=>`${names[a.family]||human(a.family)} · ${a.horizon_sessions} sessions`; let saved={};try{saved=JSON.parse(localStorage.getItem(STATE_KEY)||'{}')}catch{}let hiddenSeries=new Set(saved.hiddenSeries||[]); -function save(){try{localStorage.setItem(STATE_KEY,JSON.stringify({decisionDate:decisionSelect.value,family:familyFilter.value,horizon:horizonFilter.value,hiddenSeries:[...hiddenSeries]}))}catch{}} +function save(){try{localStorage.setItem(STATE_KEY,JSON.stringify({decisionDate:decisionSelect.value,dataLatestDate:dates[0]||null,family:familyFilter.value,horizon:horizonFilter.value,hiddenSeries:[...hiddenSeries]}))}catch{}} function option(value,label){const node=document.createElement('option');node.value=value;node.textContent=label;return node} -const dates=[...new Set(data.arms.map(a=>a.decision_date))].sort().reverse();dates.forEach(d=>decisionSelect.appendChild(option(d,d)));decisionSelect.value=dates.includes(saved.decisionDate)?saved.decisionDate:dates[0]||'';familyFilter.value=['ridge','xgb_ranker'].includes(saved.family)?saved.family:'all';horizonFilter.value=['5','10','20'].includes(saved.horizon)?saved.horizon:'all'; +const dates=[...new Set(data.arms.map(a=>a.decision_date))].sort().reverse();dates.forEach(d=>decisionSelect.appendChild(option(d,d)));const savedDateMatchesCurrentDataset=saved.dataLatestDate===dates[0];decisionSelect.value=savedDateMatchesCurrentDataset&&dates.includes(saved.decisionDate)?saved.decisionDate:dates[0]||'';familyFilter.value=['ridge','xgb_ranker'].includes(saved.family)?saved.family:'all';horizonFilter.value=['5','10','20'].includes(saved.horizon)?saved.horizon:'all'; function decisionArms(){return data.arms.filter(a=>a.decision_date===decisionSelect.value)} function visibleArms(){return decisionArms().filter(a=>(familyFilter.value==='all'||a.family===familyFilter.value)&&(horizonFilter.value==='all'||String(a.horizon_sessions)===horizonFilter.value))} function table(rows,columns,sortState){if(!rows.length)return'
No matching recordsAdjust the filters or wait for the required outcome window.
';const head=columns.map(([key,label])=>``).join(''),body=rows.map(row=>`${columns.map(([key])=>{const value=row[key],shown=key.includes('return')||key==='weight'||key==='sentiment_coverage'?fmtPct(value):key==='score'?fmtNum(value,5):value??'—';return`${shown}`}).join('')}`).join('');return`
${head}${body}
`} function renderNotice(){const arms=decisionArms(),pending=arms.filter(a=>a.status==='pending').length,next=[...new Set(arms.map(a=>a.expected_entry_date).filter(Boolean))].sort()[0],policy=data.rebalance_policy||{};document.getElementById('noticeTitle').textContent=`${pending} frozen forecasts ready for the next session`;document.getElementById('noticeCopy').textContent=`Decision ${decisionSelect.value} · expected entry ${next||'pending'} · partial marks update session by session; final labels wait for full maturity.`;document.getElementById('policyBadge').textContent=`${human(policy.status||'under audit')} · buffer ${policy.buffer_enabled?'on':'off'}`;document.getElementById('heroDate').textContent=`Latest decision · ${dates[0]||'n/a'}`} function renderKpis(){const arms=visibleArms(),matured=data.matured.filter(x=>!decisionSelect.value||x.decision_date<=decisionSelect.value),audit=data.input_audit||{},universe=data.universe||{},items=[['Active arms',arms.length,'#3156d3',`${decisionArms().length} total for this decision`],['Matured forecasts',matured.length,'#7559c7',matured.length?'Prospective outcomes available':'First outcomes still pending'],['Eligible universe',universe.final_names,'#0f9f8f',`${universe.current_liquid_names??0} liquid names`],['Sentiment coverage',arms[0]?.sentiment_coverage,'#d89614',`${audit.processed_news_rows??0} recent articles processed`]];document.getElementById('kpis').innerHTML=items.map(([label,value,color,detail],i)=>`
${label}${i===3?fmtPct(value):value??'n/a'}${detail}
`).join('');document.getElementById('dataStatus').textContent=`${dates.length} decision date${dates.length===1?'':'s'} · ${data.arms.length} frozen forecasts`} function strategyGroups(){const by=new Map,spyByDate=new Map;data.strategy_returns.forEach(row=>{const id=`${row.family}_h${row.horizon_sessions}`;if(!by.has(id))by.set(id,[]);by.get(id).push(row);if(Number.isFinite(Number(row.spy_return))&&!spyByDate.has(row.return_date))spyByDate.set(row.return_date,{return_date:row.return_date,gross_return:Number(row.spy_return)})});if(spyByDate.size)by.set('spy',[...spyByDate.values()]);return [...by.entries()].map(([id,rows])=>({id,rows:rows.sort((a,b)=>String(a.return_date).localeCompare(String(b.return_date)))}))} -function renderStrategy(){const arms=visibleArms(),visibleIds=new Set([...arms.map(a=>`${a.family}_h${a.horizon_sessions}`),'spy']),groups=strategyGroups().filter(g=>visibleIds.has(g.id)),legend=document.getElementById('strategyLegend');document.getElementById('strategyProgress').innerHTML=arms.map(a=>{const rows=data.strategy_returns.filter(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date).sort((x,y)=>String(x.return_date).localeCompare(String(y.return_date))),mark=(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date),observed=rows.length,cumulative=rows.reduce((value,row)=>value*(1+Number(row.gross_return)),1)-1,spyCumulative=rows.reduce((value,row)=>Number.isFinite(Number(row.spy_return))?value*(1+Number(row.spy_return)):value,1)-1,asOf=data.experiment.decision_date,status=observed?`${fmtPct(cumulative)} interim · ${fmtPct(cumulative-spyCumulative)} vs SPY`:mark?`Entry-day close mark ${fmtPct(mark.gross_mark_to_close)} · ${fmtPct(mark.active_mark_vs_spy)} vs SPY`:(asOf
${armLabel(a)}${observed}/${a.horizon_sessions} complete sessions
${status} · complete label ${a.expected_maturity_date}`}).join('');legend.innerHTML=groups.map(g=>``).join('');legend.querySelectorAll('[data-series]').forEach(b=>b.addEventListener('click',()=>{hiddenSeries.has(b.dataset.series)?hiddenSeries.delete(b.dataset.series):hiddenSeries.add(b.dataset.series);renderStrategy();save()}));const visible=groups.filter(g=>!hiddenSeries.has(g.id));if(!visible.length){const entries=arms.map(a=>a.expected_entry_date).filter(Boolean).sort(),marks=arms.map(a=>(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date)).filter(Boolean),message=marks.length?'Official entry-day close marks are shown above. The cumulative chart begins after the first complete open-to-open session.':`The ${decisionSelect.value} close decision first becomes invested at the ${entries[0]||'next configured'} open.`;document.getElementById('strategyChart').innerHTML=`
No complete open-to-open session yet${message}
`;return}const lines=visible.map(g=>{let cumulative=1;return{id:g.id,points:g.rows.map(r=>({date:r.return_date,value:(cumulative*=1+Number(r.gross_return))-1}))}}),stamps=[...new Set(lines.flatMap(l=>l.points.map(p=>p.date)))].sort(),vals=lines.flatMap(l=>l.points.map(p=>p.value)),min=Math.min(0,...vals),max=Math.max(0,...vals),pad=Math.max((max-min)*.14,.001),w=1180,h=360,left=66,right=22,top=18,bottom=48,pw=w-left-right,ph=h-top-bottom,x=s=>left+stamps.indexOf(s)/Math.max(1,stamps.length-1)*pw,y=v=>top+(1-(v-(min-pad))/((max+pad)-(min-pad)||1))*ph,ticks=[0,.25,.5,.75,1].map(f=>max+pad-f*((max+pad)-(min-pad))),grid=ticks.map(v=>`${fmtPct(v)}`).join(''),paths=lines.map(line=>``).join('');document.getElementById('strategyChart').innerHTML=`${grid}${paths}`} +function renderStrategy(){const arms=visibleArms(),visibleIds=new Set([...arms.map(a=>`${a.family}_h${a.horizon_sessions}`),'spy']),groups=strategyGroups().filter(g=>visibleIds.has(g.id)),legend=document.getElementById('strategyLegend');document.getElementById('strategyProgress').innerHTML=arms.map(a=>{const rows=data.strategy_returns.filter(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date).sort((x,y)=>String(x.return_date).localeCompare(String(y.return_date))),mark=(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date),completed=rows.length,observed=completed||mark?Math.max(completed,1):0,cumulative=rows.reduce((value,row)=>value*(1+Number(row.gross_return)),1)-1,spyCumulative=rows.reduce((value,row)=>Number.isFinite(Number(row.spy_return))?value*(1+Number(row.spy_return)):value,1)-1,asOf=data.experiment.decision_date,progressLabel=completed?`${completed}/${a.horizon_sessions} complete sessions`:mark?`1/${a.horizon_sessions} observed (partial) · 0 complete`:`0/${a.horizon_sessions} observed`,status=completed?`${fmtPct(cumulative)} interim · ${fmtPct(cumulative-spyCumulative)} vs SPY`:mark?`Entry-day close mark ${fmtPct(mark.gross_mark_to_close)} · ${fmtPct(mark.active_mark_vs_spy)} vs SPY`:(asOf
${armLabel(a)}${progressLabel}
${status} · complete label ${a.expected_maturity_date}`}).join('');legend.innerHTML=groups.map(g=>``).join('');legend.querySelectorAll('[data-series]').forEach(b=>b.addEventListener('click',()=>{hiddenSeries.has(b.dataset.series)?hiddenSeries.delete(b.dataset.series):hiddenSeries.add(b.dataset.series);renderStrategy();save()}));const visible=groups.filter(g=>!hiddenSeries.has(g.id));if(!visible.length){const entries=arms.map(a=>a.expected_entry_date).filter(Boolean).sort(),marks=arms.map(a=>(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date)).filter(Boolean),message=marks.length?'Official entry-day close marks are shown above as one observed partial day. The cumulative chart begins after the first complete open-to-open session.':`The ${decisionSelect.value} close decision first becomes invested at the ${entries[0]||'next configured'} open.`;document.getElementById('strategyChart').innerHTML=`
No complete open-to-open session yet${message}
`;return}const lines=visible.map(g=>{let cumulative=1;return{id:g.id,points:g.rows.map(r=>({date:r.return_date,value:(cumulative*=1+Number(r.gross_return))-1}))}}),stamps=[...new Set(lines.flatMap(l=>l.points.map(p=>p.date)))].sort(),vals=lines.flatMap(l=>l.points.map(p=>p.value)),min=Math.min(0,...vals),max=Math.max(0,...vals),pad=Math.max((max-min)*.14,.001),w=1180,h=360,left=66,right=22,top=18,bottom=48,pw=w-left-right,ph=h-top-bottom,x=s=>left+stamps.indexOf(s)/Math.max(1,stamps.length-1)*pw,y=v=>top+(1-(v-(min-pad))/((max+pad)-(min-pad)||1))*ph,ticks=[0,.25,.5,.75,1].map(f=>max+pad-f*((max+pad)-(min-pad))),grid=ticks.map(v=>`${fmtPct(v)}`).join(''),paths=lines.map(line=>``).join('');document.getElementById('strategyChart').innerHTML=`${grid}${paths}`} function renderHorizons(){const all=decisionArms(),cards=[5,10,20].map(h=>{const pair=all.filter(a=>a.horizon_sessions===h).sort((a,b)=>a.family.localeCompare(b.family)),slices=pair.map(a=>`
${names[a.family]}${fmtPct(a.validation?.top40_raw_excess)}Validation top-40 excess
Rank IC ${fmtNum(a.validation?.rank_ic)} · ${a.feature_count} features
`).join(''),maturity=pair[0]?.expected_maturity_date||'pending';return`

${h}-session horizon

Refit every ${h}
${slices}
Expected first maturity · ${maturity}
`});document.getElementById('horizonGrid').innerHTML=cards.join('')} function renderModels(){const node=document.getElementById('modelGrid');node.innerHTML=visibleArms().map(a=>{const v=a.validation||{},features=a.features.slice(0,12).map(f=>`${human(f)}`).join(''),extra=Math.max(0,a.features.length-12),shap=a.shap_summary||[],max=Math.max(...shap.map(x=>Number(x.mean_abs_shap)||0),0),chart=shap.length?`
${human(shap[0].method)} · ${shap[0].explanation_rows} current-cohort rows · mean absolute SHAP
${shap.slice(0,12).map(x=>`
${human(x.feature)}
${fmtNum(x.mean_abs_shap,4)}
`).join('')}`:`
SHAP was not captured for this frozen vintage.
The July 29 artifacts remain immutable. Genuine ${a.family==='ridge'?'linear SHAP':'TreeSHAP'} summaries will be saved with each new forecast; coefficients or XGBoost gain are not substituted or mislabeled as SHAP.
`;return`

${armLabel(a)}

${a.refitted_today?'Refit today':'Frozen model'}
Training ${a.training_start||'n/a'} to ${a.training_end||'n/a'} · ${Number(a.training_rows||0).toLocaleString()} rows · ${v.completed_trials||'n/a'} eligible trials
Top-40 excess${fmtPct(v.top40_raw_excess)}
Rank IC${fmtNum(v.rank_ic)}
Positive IC${fmtPct(v.positive_ic_rate)}
Selected ${a.feature_count} features · ${human(v.selection_metric||'n/a')}
${features}${extra?`+${extra} more`:''}
`}).join('')||'
No visible model specificationsChange the family or horizon filter.
';node.querySelectorAll('[data-explain]').forEach(button=>button.addEventListener('click',()=>{const panel=node.querySelector(`[data-explain-panel="${button.dataset.explain}"]`),opening=panel.hidden;panel.hidden=!opening;button.setAttribute('aria-expanded',String(opening));button.textContent=opening?'Hide SHAP feature importance':'Show SHAP feature importance'}))} function renderHealth(){const audit=data.input_audit||{},universe=data.universe||{},survival=data.survival_validation||{},tactical=data.tactical_orders||[],cards=[['Market data',audit.market_end===data.experiment.decision_date?'Current':'Review',`${audit.downloaded_stock_tickers??0}/${audit.requested_stock_tickers??0} histories · through ${audit.market_end||'n/a'}`,audit.market_end===data.experiment.decision_date],['News + NLP',audit.finbert_fallback_permitted===false?'Strict FinBERT':'Review',`${audit.processed_news_rows??0} articles · ${audit.nlp_feature_tickers??0} tickers`,audit.finbert_fallback_permitted===false],['Universe',`${universe.final_names??0} eligible`,`Hard exclusions ${universe.hard_exclusions??0} · ML exclusions ${universe.ml_exclusions??0}`,Number(universe.final_names)>=40],['Tactical routing',`${tactical.length} orders`,tactical.length?'Review routed orders':'No event passed every frozen gate',true],['Survival ML',survival.activated?'Active':'Shadow only',survival.activated?'Activation gates passed':human(survival.reason||'not activated'),!survival.activated],['Artifacts',`${decisionArms().length} forecasts`,`${decisionArms().filter(a=>a.status==='pending').length} pending · immutable manifests`,decisionArms().length===6],['Sentiment freshness',audit.latest_news_age_hours!=null?`${Number(audit.latest_news_age_hours).toFixed(2)} hours`:'n/a',`Latest publication ${audit.latest_news_publication||'n/a'}`,Number(audit.latest_news_age_hours)<=72],['Failures',`${(data.runs.at(-1)?.model_failures)||0} model failures`,`Latest cycle ${data.runs.at(-1)?.status||'n/a'}`,!(data.runs.at(-1)?.model_failures)]],node=document.getElementById('healthGrid');node.innerHTML=cards.map(([label,value,detail,ok])=>`
${label}
${value}${detail}
`).join('');const warnings=[];if(!survival.activated)warnings.push('Survival ML remains shadow-only; no stock is excluded by an unvalidated threshold.');if(data.rebalance_policy?.status==='under_audit')warnings.push('Full top-40 refresh remains under audit; the rank/score buffer has not been activated.');if(!data.matured.length)warnings.push('No v2 forecast has matured. Today’s successful run is operational evidence, not realized alpha.');document.getElementById('warnings').innerHTML=warnings.map(x=>`
${x}
`).join('')} function renderMatured(){const rows=data.matured.filter(x=>(familyFilter.value==='all'||x.family===familyFilter.value)&&(horizonFilter.value==='all'||String(x.horizon_sessions)===horizonFilter.value));document.getElementById('maturedTable').innerHTML=table(rows,[['decision_date','Decision'],['family','Model'],['horizon_sessions','Horizon'],['selected_return','Selected return'],['active_return_vs_universe','Excess vs universe'],['active_return_vs_spy','Excess vs SPY']],null)} function renderAll(){renderNotice();renderKpis();renderStrategy();renderHorizons();renderModels();renderHealth();renderMatured();save()} [decisionSelect,familyFilter,horizonFilter].forEach(node=>node.addEventListener('change',renderAll));document.getElementById('refreshData').addEventListener('click',()=>{save();location.reload()});renderAll(); - \ No newline at end of file + diff --git a/src/experiments/live_forward_20d_dashboard.py b/src/experiments/live_forward_20d_dashboard.py index 3fd0b00..0ef0c6a 100644 --- a/src/experiments/live_forward_20d_dashboard.py +++ b/src/experiments/live_forward_20d_dashboard.py @@ -302,7 +302,7 @@ def horizon_dashboard_html(payload: dict[str, Any]) -> str:
Frozen evidence loaded
-

Prospective strategy evidence

An official close mark appears on the entry day. Complete session counts and the cumulative chart advance only when a new open-to-open interval is observable.

The decision session is not a holding session. A July 29 close signal enters at the July 30 open. Its July 30 close can provide an entry-day mark, but the first complete open-to-open session is available at the July 31 open. Entry-day marks are never mixed with validation estimates or final horizon outcomes.
+

Prospective strategy evidence

An official close mark appears on the entry day. It is shown as one observed partial day; the completed open-to-open counter and cumulative chart advance at the next open.

The decision session is not a holding session. A July 29 close signal enters at the July 30 open. At the July 30 close, the dashboard reports 1/5 observed (partial) and the official entry-day mark, while retaining 0 complete open-to-open sessions. The first complete interval is available at the July 31 open. Entry-day marks are never mixed with validation estimates or final horizon outcomes.

Horizon comparison

Paired Ridge and XGBoost development evidence with each arm’s own purge, feature subset, refit interval, and maturity date.

Selected model specifications

The winning development configuration for each visible arm. Open a model’s explainability panel to inspect genuine SHAP importance when that frozen vintage captured it.

Pipeline and risk controls

Freshness, universe, NLP, tactical routing, survival guardrails, and artifact integrity for the selected decision date.

@@ -313,16 +313,16 @@ def horizon_dashboard_html(payload: dict[str, Any]) -> str: const data=JSON.parse(document.getElementById('dashboard-data').textContent),decisionSelect=document.getElementById('decisionSelect'),familyFilter=document.getElementById('familyFilter'),horizonFilter=document.getElementById('horizonFilter'),STATE_KEY='live-forward-horizon-dashboard-v1'; const COLORS={ridge:'#3156d3',xgb_ranker:'#0f9f8f',ridge_h5:'#7290eb',ridge_h10:'#4f70dc',ridge_h20:'#3156d3',xgb_ranker_h5:'#70c8bd',xgb_ranker_h10:'#3eb2a4',xgb_ranker_h20:'#0f9f8f',spy:'#d89614'},names={ridge:'Ridge',xgb_ranker:'XGBoost'},fmtPct=v=>v==null||!Number.isFinite(Number(v))?'n/a':(Number(v)*100).toFixed(2)+'%',fmtNum=(v,d=3)=>v==null||!Number.isFinite(Number(v))?'n/a':Number(v).toFixed(d),human=v=>String(v??'').replaceAll('_',' ').replace(/\b\w/g,c=>c.toUpperCase()),tone=v=>Number(v)>=0?'pos':'neg',armColor=a=>COLORS[`${a.family}_h${a.horizon_sessions}`]||COLORS[a.family],armLabel=a=>`${names[a.family]||human(a.family)} · ${a.horizon_sessions} sessions`; let saved={};try{saved=JSON.parse(localStorage.getItem(STATE_KEY)||'{}')}catch{}let hiddenSeries=new Set(saved.hiddenSeries||[]); -function save(){try{localStorage.setItem(STATE_KEY,JSON.stringify({decisionDate:decisionSelect.value,family:familyFilter.value,horizon:horizonFilter.value,hiddenSeries:[...hiddenSeries]}))}catch{}} +function save(){try{localStorage.setItem(STATE_KEY,JSON.stringify({decisionDate:decisionSelect.value,dataLatestDate:dates[0]||null,family:familyFilter.value,horizon:horizonFilter.value,hiddenSeries:[...hiddenSeries]}))}catch{}} function option(value,label){const node=document.createElement('option');node.value=value;node.textContent=label;return node} -const dates=[...new Set(data.arms.map(a=>a.decision_date))].sort().reverse();dates.forEach(d=>decisionSelect.appendChild(option(d,d)));decisionSelect.value=dates.includes(saved.decisionDate)?saved.decisionDate:dates[0]||'';familyFilter.value=['ridge','xgb_ranker'].includes(saved.family)?saved.family:'all';horizonFilter.value=['5','10','20'].includes(saved.horizon)?saved.horizon:'all'; +const dates=[...new Set(data.arms.map(a=>a.decision_date))].sort().reverse();dates.forEach(d=>decisionSelect.appendChild(option(d,d)));const savedDateMatchesCurrentDataset=saved.dataLatestDate===dates[0];decisionSelect.value=savedDateMatchesCurrentDataset&&dates.includes(saved.decisionDate)?saved.decisionDate:dates[0]||'';familyFilter.value=['ridge','xgb_ranker'].includes(saved.family)?saved.family:'all';horizonFilter.value=['5','10','20'].includes(saved.horizon)?saved.horizon:'all'; function decisionArms(){return data.arms.filter(a=>a.decision_date===decisionSelect.value)} function visibleArms(){return decisionArms().filter(a=>(familyFilter.value==='all'||a.family===familyFilter.value)&&(horizonFilter.value==='all'||String(a.horizon_sessions)===horizonFilter.value))} function table(rows,columns,sortState){if(!rows.length)return'
No matching recordsAdjust the filters or wait for the required outcome window.
';const head=columns.map(([key,label])=>``).join(''),body=rows.map(row=>`${columns.map(([key])=>{const value=row[key],shown=key.includes('return')||key==='weight'||key==='sentiment_coverage'?fmtPct(value):key==='score'?fmtNum(value,5):value??'—';return`${shown}`}).join('')}`).join('');return`
${head}${body}
`} function renderNotice(){const arms=decisionArms(),pending=arms.filter(a=>a.status==='pending').length,next=[...new Set(arms.map(a=>a.expected_entry_date).filter(Boolean))].sort()[0],policy=data.rebalance_policy||{};document.getElementById('noticeTitle').textContent=`${pending} frozen forecasts ready for the next session`;document.getElementById('noticeCopy').textContent=`Decision ${decisionSelect.value} · expected entry ${next||'pending'} · partial marks update session by session; final labels wait for full maturity.`;document.getElementById('policyBadge').textContent=`${human(policy.status||'under audit')} · buffer ${policy.buffer_enabled?'on':'off'}`;document.getElementById('heroDate').textContent=`Latest decision · ${dates[0]||'n/a'}`} function renderKpis(){const arms=visibleArms(),matured=data.matured.filter(x=>!decisionSelect.value||x.decision_date<=decisionSelect.value),audit=data.input_audit||{},universe=data.universe||{},items=[['Active arms',arms.length,'#3156d3',`${decisionArms().length} total for this decision`],['Matured forecasts',matured.length,'#7559c7',matured.length?'Prospective outcomes available':'First outcomes still pending'],['Eligible universe',universe.final_names,'#0f9f8f',`${universe.current_liquid_names??0} liquid names`],['Sentiment coverage',arms[0]?.sentiment_coverage,'#d89614',`${audit.processed_news_rows??0} recent articles processed`]];document.getElementById('kpis').innerHTML=items.map(([label,value,color,detail],i)=>`
${label}${i===3?fmtPct(value):value??'n/a'}${detail}
`).join('');document.getElementById('dataStatus').textContent=`${dates.length} decision date${dates.length===1?'':'s'} · ${data.arms.length} frozen forecasts`} function strategyGroups(){const by=new Map,spyByDate=new Map;data.strategy_returns.forEach(row=>{const id=`${row.family}_h${row.horizon_sessions}`;if(!by.has(id))by.set(id,[]);by.get(id).push(row);if(Number.isFinite(Number(row.spy_return))&&!spyByDate.has(row.return_date))spyByDate.set(row.return_date,{return_date:row.return_date,gross_return:Number(row.spy_return)})});if(spyByDate.size)by.set('spy',[...spyByDate.values()]);return [...by.entries()].map(([id,rows])=>({id,rows:rows.sort((a,b)=>String(a.return_date).localeCompare(String(b.return_date)))}))} -function renderStrategy(){const arms=visibleArms(),visibleIds=new Set([...arms.map(a=>`${a.family}_h${a.horizon_sessions}`),'spy']),groups=strategyGroups().filter(g=>visibleIds.has(g.id)),legend=document.getElementById('strategyLegend');document.getElementById('strategyProgress').innerHTML=arms.map(a=>{const rows=data.strategy_returns.filter(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date).sort((x,y)=>String(x.return_date).localeCompare(String(y.return_date))),mark=(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date),observed=rows.length,cumulative=rows.reduce((value,row)=>value*(1+Number(row.gross_return)),1)-1,spyCumulative=rows.reduce((value,row)=>Number.isFinite(Number(row.spy_return))?value*(1+Number(row.spy_return)):value,1)-1,asOf=data.experiment.decision_date,status=observed?`${fmtPct(cumulative)} interim · ${fmtPct(cumulative-spyCumulative)} vs SPY`:mark?`Entry-day close mark ${fmtPct(mark.gross_mark_to_close)} · ${fmtPct(mark.active_mark_vs_spy)} vs SPY`:(asOf
${armLabel(a)}${observed}/${a.horizon_sessions} complete sessions
${status} · complete label ${a.expected_maturity_date}`}).join('');legend.innerHTML=groups.map(g=>``).join('');legend.querySelectorAll('[data-series]').forEach(b=>b.addEventListener('click',()=>{hiddenSeries.has(b.dataset.series)?hiddenSeries.delete(b.dataset.series):hiddenSeries.add(b.dataset.series);renderStrategy();save()}));const visible=groups.filter(g=>!hiddenSeries.has(g.id));if(!visible.length){const entries=arms.map(a=>a.expected_entry_date).filter(Boolean).sort(),marks=arms.map(a=>(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date)).filter(Boolean),message=marks.length?'Official entry-day close marks are shown above. The cumulative chart begins after the first complete open-to-open session.':`The ${decisionSelect.value} close decision first becomes invested at the ${entries[0]||'next configured'} open.`;document.getElementById('strategyChart').innerHTML=`
No complete open-to-open session yet${message}
`;return}const lines=visible.map(g=>{let cumulative=1;return{id:g.id,points:g.rows.map(r=>({date:r.return_date,value:(cumulative*=1+Number(r.gross_return))-1}))}}),stamps=[...new Set(lines.flatMap(l=>l.points.map(p=>p.date)))].sort(),vals=lines.flatMap(l=>l.points.map(p=>p.value)),min=Math.min(0,...vals),max=Math.max(0,...vals),pad=Math.max((max-min)*.14,.001),w=1180,h=360,left=66,right=22,top=18,bottom=48,pw=w-left-right,ph=h-top-bottom,x=s=>left+stamps.indexOf(s)/Math.max(1,stamps.length-1)*pw,y=v=>top+(1-(v-(min-pad))/((max+pad)-(min-pad)||1))*ph,ticks=[0,.25,.5,.75,1].map(f=>max+pad-f*((max+pad)-(min-pad))),grid=ticks.map(v=>`${fmtPct(v)}`).join(''),paths=lines.map(line=>``).join('');document.getElementById('strategyChart').innerHTML=`${grid}${paths}`} +function renderStrategy(){const arms=visibleArms(),visibleIds=new Set([...arms.map(a=>`${a.family}_h${a.horizon_sessions}`),'spy']),groups=strategyGroups().filter(g=>visibleIds.has(g.id)),legend=document.getElementById('strategyLegend');document.getElementById('strategyProgress').innerHTML=arms.map(a=>{const rows=data.strategy_returns.filter(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date).sort((x,y)=>String(x.return_date).localeCompare(String(y.return_date))),mark=(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date),completed=rows.length,observed=completed||mark?Math.max(completed,1):0,cumulative=rows.reduce((value,row)=>value*(1+Number(row.gross_return)),1)-1,spyCumulative=rows.reduce((value,row)=>Number.isFinite(Number(row.spy_return))?value*(1+Number(row.spy_return)):value,1)-1,asOf=data.experiment.decision_date,progressLabel=completed?`${completed}/${a.horizon_sessions} complete sessions`:mark?`1/${a.horizon_sessions} observed (partial) · 0 complete`:`0/${a.horizon_sessions} observed`,status=completed?`${fmtPct(cumulative)} interim · ${fmtPct(cumulative-spyCumulative)} vs SPY`:mark?`Entry-day close mark ${fmtPct(mark.gross_mark_to_close)} · ${fmtPct(mark.active_mark_vs_spy)} vs SPY`:(asOf
${armLabel(a)}${progressLabel}
${status} · complete label ${a.expected_maturity_date}`}).join('');legend.innerHTML=groups.map(g=>``).join('');legend.querySelectorAll('[data-series]').forEach(b=>b.addEventListener('click',()=>{hiddenSeries.has(b.dataset.series)?hiddenSeries.delete(b.dataset.series):hiddenSeries.add(b.dataset.series);renderStrategy();save()}));const visible=groups.filter(g=>!hiddenSeries.has(g.id));if(!visible.length){const entries=arms.map(a=>a.expected_entry_date).filter(Boolean).sort(),marks=arms.map(a=>(data.strategy_interim_marks||[]).find(r=>r.family===a.family&&Number(r.horizon_sessions)===a.horizon_sessions&&r.rebalance_decision_date===a.decision_date)).filter(Boolean),message=marks.length?'Official entry-day close marks are shown above as one observed partial day. The cumulative chart begins after the first complete open-to-open session.':`The ${decisionSelect.value} close decision first becomes invested at the ${entries[0]||'next configured'} open.`;document.getElementById('strategyChart').innerHTML=`
No complete open-to-open session yet${message}
`;return}const lines=visible.map(g=>{let cumulative=1;return{id:g.id,points:g.rows.map(r=>({date:r.return_date,value:(cumulative*=1+Number(r.gross_return))-1}))}}),stamps=[...new Set(lines.flatMap(l=>l.points.map(p=>p.date)))].sort(),vals=lines.flatMap(l=>l.points.map(p=>p.value)),min=Math.min(0,...vals),max=Math.max(0,...vals),pad=Math.max((max-min)*.14,.001),w=1180,h=360,left=66,right=22,top=18,bottom=48,pw=w-left-right,ph=h-top-bottom,x=s=>left+stamps.indexOf(s)/Math.max(1,stamps.length-1)*pw,y=v=>top+(1-(v-(min-pad))/((max+pad)-(min-pad)||1))*ph,ticks=[0,.25,.5,.75,1].map(f=>max+pad-f*((max+pad)-(min-pad))),grid=ticks.map(v=>`${fmtPct(v)}`).join(''),paths=lines.map(line=>``).join('');document.getElementById('strategyChart').innerHTML=`${grid}${paths}`} function renderHorizons(){const all=decisionArms(),cards=[5,10,20].map(h=>{const pair=all.filter(a=>a.horizon_sessions===h).sort((a,b)=>a.family.localeCompare(b.family)),slices=pair.map(a=>`
${names[a.family]}${fmtPct(a.validation?.top40_raw_excess)}Validation top-40 excess
Rank IC ${fmtNum(a.validation?.rank_ic)} · ${a.feature_count} features
`).join(''),maturity=pair[0]?.expected_maturity_date||'pending';return`

${h}-session horizon

Refit every ${h}
${slices}
Expected first maturity · ${maturity}
`});document.getElementById('horizonGrid').innerHTML=cards.join('')} function renderModels(){const node=document.getElementById('modelGrid');node.innerHTML=visibleArms().map(a=>{const v=a.validation||{},features=a.features.slice(0,12).map(f=>`${human(f)}`).join(''),extra=Math.max(0,a.features.length-12),shap=a.shap_summary||[],max=Math.max(...shap.map(x=>Number(x.mean_abs_shap)||0),0),chart=shap.length?`
${human(shap[0].method)} · ${shap[0].explanation_rows} current-cohort rows · mean absolute SHAP
${shap.slice(0,12).map(x=>`
${human(x.feature)}
${fmtNum(x.mean_abs_shap,4)}
`).join('')}`:`
SHAP was not captured for this frozen vintage.
The July 29 artifacts remain immutable. Genuine ${a.family==='ridge'?'linear SHAP':'TreeSHAP'} summaries will be saved with each new forecast; coefficients or XGBoost gain are not substituted or mislabeled as SHAP.
`;return`

${armLabel(a)}

${a.refitted_today?'Refit today':'Frozen model'}
Training ${a.training_start||'n/a'} to ${a.training_end||'n/a'} · ${Number(a.training_rows||0).toLocaleString()} rows · ${v.completed_trials||'n/a'} eligible trials
Top-40 excess${fmtPct(v.top40_raw_excess)}
Rank IC${fmtNum(v.rank_ic)}
Positive IC${fmtPct(v.positive_ic_rate)}
Selected ${a.feature_count} features · ${human(v.selection_metric||'n/a')}
${features}${extra?`+${extra} more`:''}
`}).join('')||'
No visible model specificationsChange the family or horizon filter.
';node.querySelectorAll('[data-explain]').forEach(button=>button.addEventListener('click',()=>{const panel=node.querySelector(`[data-explain-panel="${button.dataset.explain}"]`),opening=panel.hidden;panel.hidden=!opening;button.setAttribute('aria-expanded',String(opening));button.textContent=opening?'Hide SHAP feature importance':'Show SHAP feature importance'}))} function renderHealth(){const audit=data.input_audit||{},universe=data.universe||{},survival=data.survival_validation||{},tactical=data.tactical_orders||[],cards=[['Market data',audit.market_end===data.experiment.decision_date?'Current':'Review',`${audit.downloaded_stock_tickers??0}/${audit.requested_stock_tickers??0} histories · through ${audit.market_end||'n/a'}`,audit.market_end===data.experiment.decision_date],['News + NLP',audit.finbert_fallback_permitted===false?'Strict FinBERT':'Review',`${audit.processed_news_rows??0} articles · ${audit.nlp_feature_tickers??0} tickers`,audit.finbert_fallback_permitted===false],['Universe',`${universe.final_names??0} eligible`,`Hard exclusions ${universe.hard_exclusions??0} · ML exclusions ${universe.ml_exclusions??0}`,Number(universe.final_names)>=40],['Tactical routing',`${tactical.length} orders`,tactical.length?'Review routed orders':'No event passed every frozen gate',true],['Survival ML',survival.activated?'Active':'Shadow only',survival.activated?'Activation gates passed':human(survival.reason||'not activated'),!survival.activated],['Artifacts',`${decisionArms().length} forecasts`,`${decisionArms().filter(a=>a.status==='pending').length} pending · immutable manifests`,decisionArms().length===6],['Sentiment freshness',audit.latest_news_age_hours!=null?`${Number(audit.latest_news_age_hours).toFixed(2)} hours`:'n/a',`Latest publication ${audit.latest_news_publication||'n/a'}`,Number(audit.latest_news_age_hours)<=72],['Failures',`${(data.runs.at(-1)?.model_failures)||0} model failures`,`Latest cycle ${data.runs.at(-1)?.status||'n/a'}`,!(data.runs.at(-1)?.model_failures)]],node=document.getElementById('healthGrid');node.innerHTML=cards.map(([label,value,detail,ok])=>`
${label}
${value}${detail}
`).join('');const warnings=[];if(!survival.activated)warnings.push('Survival ML remains shadow-only; no stock is excluded by an unvalidated threshold.');if(data.rebalance_policy?.status==='under_audit')warnings.push('Full top-40 refresh remains under audit; the rank/score buffer has not been activated.');if(!data.matured.length)warnings.push('No v2 forecast has matured. Today’s successful run is operational evidence, not realized alpha.');document.getElementById('warnings').innerHTML=warnings.map(x=>`
${x}
`).join('')} diff --git a/tests/test_live_forward_20d_dashboard.py b/tests/test_live_forward_20d_dashboard.py index aa08ba2..911fcf9 100644 --- a/tests/test_live_forward_20d_dashboard.py +++ b/tests/test_live_forward_20d_dashboard.py @@ -172,5 +172,9 @@ def test_dashboard_html_exposes_v2_information_architecture() -> None: assert "never mixed with validation estimates" in html assert "Show SHAP feature importance" in html assert "live-forward-horizon-dashboard-v1" in html + assert "saved.dataLatestDate===dates[0]" in html + assert "dataLatestDate:dates[0]||null" in html + assert "1/5 observed (partial)" in html + assert "0 complete open-to-open sessions" in html assert "width=device-width,initial-scale=1" in html assert "__DASHBOARD_PAYLOAD__" not in html