From ed8bac647b1009f24350bb07d8c50d19258ae945 Mon Sep 17 00:00:00 2001 From: HanSun103 Date: Thu, 30 Jul 2026 19:48:47 -0400 Subject: [PATCH 1/3] Reset horizon dashboard to latest dataset --- site/horizons.html | 4 ++-- src/experiments/live_forward_20d_dashboard.py | 4 ++-- tests/test_live_forward_20d_dashboard.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/site/horizons.html b/site/horizons.html index 27c24fa..0623a08 100644 --- a/site/horizons.html +++ b/site/horizons.html @@ -46,9 +46,9 @@ 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}
`} diff --git a/src/experiments/live_forward_20d_dashboard.py b/src/experiments/live_forward_20d_dashboard.py index 3fd0b00..2ce4ec0 100644 --- a/src/experiments/live_forward_20d_dashboard.py +++ b/src/experiments/live_forward_20d_dashboard.py @@ -313,9 +313,9 @@ 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}
`} diff --git a/tests/test_live_forward_20d_dashboard.py b/tests/test_live_forward_20d_dashboard.py index aa08ba2..fa9a732 100644 --- a/tests/test_live_forward_20d_dashboard.py +++ b/tests/test_live_forward_20d_dashboard.py @@ -172,5 +172,7 @@ 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 "width=device-width,initial-scale=1" in html assert "__DASHBOARD_PAYLOAD__" not in html From 8175706456cf8e7ce541f69576dde051cd2aae2a Mon Sep 17 00:00:00 2001 From: HanSun103 Date: Thu, 30 Jul 2026 19:50:06 -0400 Subject: [PATCH 2/3] Reconcile published dashboard manifest --- site/data/manifest.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/data/manifest.json b/site/data/manifest.json index 5ea3be5..276650e 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-30T23:49:59.329147+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": "38f310c1e415c2fd4679d628cc2d29d6fe43e89a4f5861fa05087960cdab620b", + "horizon_dashboard_bytes": 128971, "horizon_forecast_arms": 12, "source_policy": "sanitized_live_forward_outputs_only" } \ No newline at end of file From cbb06056d5028c406bf5e4872941d75c6df8dc95 Mon Sep 17 00:00:00 2001 From: HanSun103 Date: Thu, 30 Jul 2026 20:06:32 -0400 Subject: [PATCH 3/3] Show entry-day marks as partial progress --- site/data/manifest.json | 6 +++--- site/horizons.html | 6 +++--- src/experiments/live_forward_20d_dashboard.py | 4 ++-- tests/test_live_forward_20d_dashboard.py | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/site/data/manifest.json b/site/data/manifest.json index 276650e..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:49:59.329147+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": "38f310c1e415c2fd4679d628cc2d29d6fe43e89a4f5861fa05087960cdab620b", - "horizon_dashboard_bytes": 128971, + "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 0623a08..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.

@@ -55,11 +55,11 @@ 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 2ce4ec0..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.

@@ -322,7 +322,7 @@ def horizon_dashboard_html(payload: dict[str, Any]) -> str: 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 fa9a732..911fcf9 100644 --- a/tests/test_live_forward_20d_dashboard.py +++ b/tests/test_live_forward_20d_dashboard.py @@ -174,5 +174,7 @@ def test_dashboard_html_exposes_v2_information_architecture() -> None: 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