/* * Browser-side controller for the Factor-Risk-Decomposition demo. * * The server returns precomputed results + a live synthetic-market generator as * JSON. This file renders the dashboard with Plotly.js, themed to the same Tokyo * Night palette as the rest of the site (transparent paper, Inter font). */ // Tokyo Night palette (matches app.css tokens). const COLORS = { long: "#9ECE6A", ew: "#7aa2f7", ls: "#F7768E", accent: "#BB9AF7", real: "#E0AF68", muted: "#787C99", neg: "#F7768E", pos: "#9ECE6A", bar: "#2AC3DE", grid: "rgba(120,124,153,0.18)", fg: "#A9B1D6", }; const FONT = { family: "Inter, sans-serif", size: 12, color: COLORS.fg }; const baseLayout = (extra = {}) => Object.assign({ paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", font: FONT, margin: { l: 52, r: 18, t: 16, b: 38 }, xaxis: { gridcolor: COLORS.grid, zerolinecolor: COLORS.grid, linecolor: COLORS.grid, tickfont: { size: 10 } }, yaxis: { gridcolor: COLORS.grid, zerolinecolor: COLORS.grid, linecolor: COLORS.grid, tickfont: { size: 10 } }, legend: { font: { color: COLORS.fg }, orientation: "h", y: -0.2 }, colorway: [COLORS.bar, COLORS.long, COLORS.accent, COLORS.ls, COLORS.real], }, extra); const CONFIG = { responsive: true, displayModeBar: false }; const $ = (id) => document.getElementById(id); async function fetchJson(url, opts) { const r = await fetch(url, opts); const t = await r.text(); let payload = {}; try { payload = t ? JSON.parse(t) : {}; } catch { if (!r.ok) throw new Error(t || `HTTP ${r.status}`); throw new Error(`Invalid JSON from ${url}`); } if (!r.ok) { const detail = payload.detail || payload.message || t; throw new Error(detail || `HTTP ${r.status}`); } return payload; } const pct = (x, n = 2) => (x == null || isNaN(x)) ? "—" : (x * 100).toFixed(n) + "%"; const fixed = (x, n = 2) => (x == null || isNaN(x)) ? "—" : Number(x).toFixed(n); const cls = (x) => (x == null || isNaN(x)) ? "" : (x >= 0 ? "pos" : "neg"); function tile(label, value, sub, valueClass = "") { return `
This is the average return left after controlling for market, size, value, and momentum benchmark factors. The stock-ranking IC is weak, so this alpha is treated as evidence to stress-test, not a victory lap.
`; $("hero-action").innerHTML = ""; } // ----------------------------- overview ------------------------------------ // async function renderOverview() { const d = overviewCache || await fetchJson("/api/overview"); const hl = d.headline; $("overview-metrics").innerHTML = tile("Sharpe (long-only)", fixed(hl.sharpe), `ann. return ${pct(hl.ann_return)}`) + tile("Max drawdown", pct(hl.max_drawdown), `vol ${pct(hl.ann_vol)}`) + tile("Active vs EW", pct(hl.active_annual), `IR ${fixed(hl.active_ir)}`, cls(hl.active_annual)) + tile("EW universe Sharpe", fixed(d.ew.sharpe), `ann. return ${pct(d.ew.ann_return)}`); const dates = d.equity.dates; Plotly.newPlot($("chart-equity"), [ { x: dates, y: d.equity.long, name: "Long-only", mode: "lines", line: { color: COLORS.long, width: 2 } }, { x: dates, y: d.equity.ew, name: "EW universe", mode: "lines", line: { color: COLORS.ew, width: 1.5 } }, { x: dates, y: d.equity.ls, name: "Long-short", mode: "lines", line: { color: COLORS.ls, width: 1.5, dash: "dot" } }, ], baseLayout({ yaxis: { title: "cumulative return", gridcolor: COLORS.grid }, legend: {} }), CONFIG); Plotly.newPlot($("chart-drawdown"), [ { x: d.drawdown.dates, y: d.drawdown.long, name: "drawdown", type: "scatter", fill: "tozeroy", line: { color: COLORS.ls, width: 1 }, fillcolor: "rgba(247,118,142,0.25)" }, ], baseLayout({ yaxis: { title: "drawdown", gridcolor: COLORS.grid, tickformat: ".0%" } }), CONFIG); } // ------------------------------ factors ------------------------------------ // async function renderFactors() { const d = await fetchJson("/api/factors"); const names = Object.keys(d.ic); Plotly.newPlot($("chart-ic"), [{ x: names, y: names.map((n) => d.ic[n].mean), type: "bar", marker: { color: names.map((n) => n === "momentum" ? COLORS.pos : COLORS.bar) }, text: names.map((n) => `IR=${fixed(d.ic[n].ir)}`), textposition: "outside", }], baseLayout({ yaxis: { title: "mean monthly IC", gridcolor: COLORS.grid, zerolinecolor: COLORS.muted }, shapes: [{ type: "line", x0: -0.5, x1: names.length - 0.5, y0: 0, y1: 0, line: { color: COLORS.muted, width: 1 } }], }), CONFIG); Plotly.newPlot($("chart-corr"), [{ z: d.correlation.matrix, x: d.correlation.labels, y: d.correlation.labels, type: "heatmap", colorscale: [[0, "#F7768E"], [0.5, "#1A1B26"], [1, "#9ECE6A"]], zmin: -1, zmax: 1, showscale: false, text: d.correlation.matrix.map((r) => r.map((v) => v.toFixed(2))), texttemplate: "%{text}", }], baseLayout({ margin: { l: 64, r: 18, t: 16, b: 40 } }), CONFIG); const periods = Object.keys(d.walkforward); Plotly.newPlot($("chart-walkforward"), names.map((n) => ({ name: n, type: "bar", x: periods, y: periods.map((p) => d.walkforward[p][n]), })), baseLayout({ barmode: "group", yaxis: { title: "mean IC", gridcolor: COLORS.grid, zerolinecolor: COLORS.muted }, shapes: [{ type: "line", x0: -0.5, x1: periods.length - 0.5, y0: 0, y1: 0, line: { color: COLORS.muted, width: 1 } }], }), CONFIG); } // ------------------------------- risk -------------------------------------- // async function renderRisk() { const d = await fetchJson("/api/risk"); const mp = d.mp; $("risk-metrics").innerHTML = tile("Significant factors", mp.signal_count, `above MP λ+ = ${fixed(mp.lam_plus)}`) + tile("q = T / N", fixed(mp.q), `σ² = ${fixed(mp.sigma2)}`) + tile("Systematic risk", pct(d.variance_decomp.systematic / 100), "variance in top-k subspace", "pos") + tile("Idiosyncratic risk", pct(d.variance_decomp.idiosyncratic / 100), "orthogonal complement", "neg"); const xs = d.spectrum.eigenvalue.map((_, i) => i + 1); Plotly.newPlot($("chart-scree"), [ { x: xs, y: d.spectrum.eigenvalue, type: "bar", name: "eigenvalue", marker: { color: COLORS.bar } }, ], baseLayout({ yaxis: { title: "eigenvalue", gridcolor: COLORS.grid }, xaxis: { title: "principal component", gridcolor: COLORS.grid }, shapes: [{ type: "line", x0: 0, x1: xs.length, y0: mp.lam_plus, y1: mp.lam_plus, line: { color: COLORS.real, width: 2, dash: "dash" } }], annotations: [{ x: xs.length, y: mp.lam_plus, xanchor: "right", yanchor: "bottom", text: "λ₊ (MP cutoff)", font: { color: COLORS.real, size: 10 }, showarrow: false }], }), CONFIG); Plotly.newPlot($("chart-vardecomp"), [{ labels: ["Systematic", "Idiosyncratic"], values: [d.variance_decomp.systematic, d.variance_decomp.idiosyncratic], type: "pie", hole: 0.55, marker: { colors: [COLORS.bar, COLORS.ls] }, textinfo: "label+percent", textfont: { color: COLORS.fg, size: 11 }, }], baseLayout({ margin: { l: 10, r: 10, t: 10, b: 10 } }), CONFIG); } // ------------------------------ stress ------------------------------------- // let stressDist = null; async function renderStress() { const d = await fetchJson("/api/stress"); stressDist = d; $("stress-metrics").innerHTML = tile("Real alpha (baseline)", pct(d.real_alpha), "raw-momentum pipeline", cls(d.real_alpha)) + tile("Bootstrap mean", pct(d.mean), "across 300 paths") + tile("Share ≥ real", pct(d.p_value), "higher = more typical (not a lucky path)"); drawStressDist(null); $("stress-btn").addEventListener("click", generateStress); } function drawStressDist(genAlpha) { const shapes = [{ type: "line", x0: stressDist.real_alpha, x1: stressDist.real_alpha, y0: 0, y1: 1, yref: "paper", line: { color: COLORS.real, width: 2 } }]; if (genAlpha != null && !isNaN(genAlpha)) shapes.push({ type: "line", x0: genAlpha, x1: genAlpha, y0: 0, y1: 1, yref: "paper", line: { color: COLORS.ls, width: 2, dash: "dot" } }); Plotly.newPlot($("chart-stress-dist"), [{ x: stressDist.distribution, type: "histogram", name: "synthetic α", marker: { color: COLORS.bar, opacity: 0.65 }, xbins: { size: 0.01 }, }], baseLayout({ xaxis: { title: "annualized FF alpha", tickformat: ".0%", gridcolor: COLORS.grid }, yaxis: { title: "# synthetic markets", gridcolor: COLORS.grid }, shapes, annotations: [ { x: stressDist.real_alpha, y: 1, yref: "paper", xanchor: "left", yanchor: "top", text: "real", font: { color: COLORS.real, size: 10 }, showarrow: false }, ...(genAlpha != null && !isNaN(genAlpha) ? [{ x: genAlpha, y: 0.9, yref: "paper", xanchor: "left", text: "generated", font: { color: COLORS.ls, size: 10 }, showarrow: false }] : []), ], }), CONFIG); } async function generateStress() { const btn = $("stress-btn"); const ro = $("stress-readout"); btn.disabled = true; ro.textContent = "generating…"; try { const r = await fetchJson("/api/stress", { method: "POST" }); drawStressDist(r.alpha); const xs = r.equity.map((_, i) => i); Plotly.newPlot($("chart-stress-equity"), [{ x: xs, y: r.equity, type: "scatter", mode: "lines", fill: "tozeroy", line: { color: COLORS.ls, width: 1.5 }, fillcolor: "rgba(247,118,142,0.18)", }], baseLayout({ xaxis: { title: "synthetic month", gridcolor: COLORS.grid }, yaxis: { title: "cumulative return", gridcolor: COLORS.grid }, annotations: [{ x: 0.5, y: 1.08, yref: "paper", xref: "paper", text: `α = ${pct(r.alpha)} · t = ${fixed(r.t)} · ${fixed((r.percentile || 0) * 100, 0)}th pctile`, font: { color: COLORS.real, size: 11 }, showarrow: false }], }), CONFIG); ro.textContent = `α = ${pct(r.alpha)} (${fixed((r.percentile || 0) * 100, 0)}th percentile)`; const heroRo = $("hero-stress-readout"); if (heroRo) heroRo.textContent = `Last generated: α = ${pct(r.alpha)} (${fixed((r.percentile || 0) * 100, 0)}th percentile)`; } catch (e) { ro.textContent = "error: " + e.message; } btn.disabled = false; } // ----------------------------- explorer ------------------------------------ // async function renderExplorer() { const rows = await fetchJson("/api/tickers"); const sectors = [...new Set(rows.map((r) => r.sector))].sort(); const palette = [COLORS.bar, COLORS.long, COLORS.accent, COLORS.ls, COLORS.real, "#7aa2f7", "#B4F9F8", "#e0af68", "#f7768e", "#bb9af7", "#9ece6a"]; const bySector = {}; rows.forEach((r) => (bySector[r.sector] ||= []).push(r)); const traces = sectors.map((sec, i) => ({ name: sec, type: "scatter", mode: "markers", x: bySector[sec].map((r) => r.pc1), y: bySector[sec].map((r) => r.pc2), text: bySector[sec].map((r) => r.ticker), marker: { color: palette[i % palette.length], size: bySector[sec].map((r) => 5 + 3 * Math.min(Math.abs(r.momentum || 0), 3)), opacity: 0.75, line: { width: 0 } }, })); Plotly.newPlot($("chart-scatter"), traces, baseLayout({ xaxis: { title: "PC1 (≈ market)", gridcolor: COLORS.grid }, yaxis: { title: "PC2 (≈ value)", gridcolor: COLORS.grid }, legend: { font: { color: COLORS.fg, size: 9 }, orientation: "v", x: 1.02 }, margin: { l: 52, r: 120, t: 16, b: 38 }, }), CONFIG); const sel = $("ticker-select"); rows.map((r) => r.ticker).sort().forEach((t) => { const o = document.createElement("option"); o.value = t; o.textContent = t; sel.appendChild(o); }); sel.addEventListener("change", () => showTicker(rows.find((r) => r.ticker === sel.value), sel.value)); // click a point to select that ticker $("chart-scatter").on("plotly_click", (ev) => { const p = ev.points[0]; const t = p.text; sel.value = t; showTicker(rows.find((r) => r.ticker === t), t); }); sel.value = "AAPL"; showTicker(rows.find((r) => r.ticker === "AAPL"), "AAPL"); } function showTicker(r, t) { const detail = $("ticker-detail"); detail.replaceChildren(); if (!r) { detail.textContent = `${t}: not in the balanced panel.`; return; } const ticker = document.createElement("b"); ticker.textContent = r.ticker; const momentum = document.createElement("b"); momentum.textContent = fixed(r.momentum); detail.append( ticker, document.createTextNode(` · sector: ${r.sector} · momentum z-score: `), momentum, document.createTextNode(` · PC1 (market): ${fixed(r.pc1)} · PC2 (value): ${fixed(r.pc2)}`), ); } init();