/* Slipstream dashboard — fund (automated) + informational picks (no manual trading). */ const $ = (s, el = document) => el.querySelector(s); const escapeHtml = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const usd = n => (n ?? 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); async function loadHealth() { const h = await fetch("/api/health").then(r => r.json()).catch(() => ({})); const b = $("#env-badge"); if (!h.alpaca_configured) { b.textContent = "Alpaca: not configured"; return; } b.textContent = h.alpaca_paper ? "PAPER" : "LIVE"; b.classList.add(h.alpaca_paper ? "paper" : "live"); } /* ---------- status line ---------- */ async function loadStatus() { const el = $("#statusline"); if (!el) return; const s = await fetch("/api/status").then(r => r.ok ? r.json() : null).catch(() => null); if (!s) { el.innerHTML = `Status unavailable — add Alpaca secrets on the Space`; return; } const open = s.market?.is_open; const fmt = t => t ? new Date(t).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "—"; const next = open ? `closes ${fmt(s.market.next_close)}` : `opens ${fmt(s.market.next_open)}`; const done = s.rebalanced_this_month; el.innerHTML = ` Market ${open ? "open" : "closed"} · ${next} This month: ${done ? "rebalanced" : "pending"} Last order: ${s.last_order || "none yet"} Auto-rebalance: ${s.rebalance_enabled ? "on" : "off (set REBALANCE_TOKEN)"} · monthly, 1st trading day`; } /* ---------- fund ---------- */ async function loadFund() { const resp = await fetch("/api/fund"); const hold = $("#holdings"); if (!resp.ok) { hold.innerHTML = `

${(await resp.json().catch(()=>({}))).detail || "Fund unavailable — add Alpaca secrets"}

`; return; } const p = await resp.json(); const dayPl = p.equity - p.last_equity, dayPct = p.last_equity ? dayPl / p.last_equity * 100 : 0; const invested = p.equity ? (1 - p.cash / p.equity) * 100 : 0; $("#kpis").innerHTML = [ ["Equity", `$${usd(p.equity)}`, ""], ["Day P/L", `${dayPl >= 0 ? "+" : "−"}$${usd(Math.abs(dayPl))}`, dayPl >= 0 ? "pl-pos" : "pl-neg"], ["Day %", `${dayPct >= 0 ? "▲ +" : "▼ "}${dayPct.toFixed(2)}%`, dayPct >= 0 ? "pl-pos" : "pl-neg"], ["Cash", `$${usd(p.cash)}`, ""], ["Invested", `${invested.toFixed(0)}%`, ""], ].map(([l, v, c]) => `
${l}
${v}
`).join(""); $("#holdings-n").textContent = `${p.positions.length} names`; const maxw = Math.max(0.01, ...p.positions.map(x => x.weight)); hold.innerHTML = p.positions.length ? ` ${p.positions.map(x => ``).join("")}
SymbolWeightQtyPriceP/L %Value
${escapeHtml(x.symbol)}
${(x.weight*100).toFixed(1)}%${x.qty.toFixed(2)}$${x.current.toFixed(2)} ${x.pl_pct>=0?"+":""}${x.pl_pct}% $${usd(x.market_value)}
` : "

No open positions yet — run a rebalance to build the book.

"; } /* ---------- target book ---------- */ async function loadTarget() { const t = await fetch("/api/target").then(r => r.json()).catch(() => null); if (!t) return; const on = t.book === "base"; const chip = $("#regime"); chip.textContent = t.regime; chip.classList.add(on ? "on" : "off"); $("#target").innerHTML = t.holdings.map(h => `
${escapeHtml(h.ticker)}${escapeHtml(h.sector)} ${(h.weight*100).toFixed(1)}%
`).join("") + `

${t.n_holdings} holdings · signals as-of ${escapeHtml(t.signals_as_of)} · bundle prices ${escapeHtml(t.prices_through)}` + (t.spy_through && t.spy_through !== t.prices_through ? ` · SPY live through ${escapeHtml(t.spy_through)}` : "") + `
${escapeHtml(t.rule)}

`; renderRiskScreen(t.risk_screen); } /* ---------- AI event-risk screen (shows the overlay's power) ---------- */ function renderRiskScreen(rs) { const el = $("#riskscreen"); if (!el) return; if (!rs || !rs.enabled) { el.innerHTML = ""; return; } const ex = rs.excluded || []; const head = ex.length ? `${ex.length} excluded · ${rs.cleared}/${rs.scanned} candidates cleared` : `all clear · ${rs.scanned} candidates scanned, no material adverse events`; const li = e => `
  • ${escapeHtml(e.severity)} ${escapeHtml(e.ticker)} — ${escapeHtml(e.event)} ${escapeHtml(e.category)} ${e.headline ? `“${escapeHtml(e.headline)}”` : ""}
  • `; const rows = ex.map(li).join(""); const med = (rs.flags_medium || []); const medRows = med.map(li).join(""); el.innerHTML = `
    🛡 AI Risk Screen — ${head}
    ` + (rows ? `` : "") + (medRows ? `
    Monitoring (below exclusion threshold — held):
    ` : "") + `

    A second-stage AI overlay scans the final candidates for concrete, recent adverse events — legal · regulatory/FDA · geopolitical/trade · operational · competitive — and hard-excludes only critical hits (threats to the company itself; replaced by the next-ranked name). High and medium events are shown above and held. Ongoing, already-priced risks are deliberately ignored.

    `; } async function previewRebalance() { const out = $("#rebal-out"); out.textContent = "Computing plan…"; const r = await fetch("/api/rebalance?dry=1", { method: "POST" }).then(r => r.json()).catch(() => null); if (!r || r.detail) { out.innerHTML = `${r?.detail || "unavailable"}`; return; } const cashLine = r.projected_cash != null ? ` · cash after: $${usd(r.projected_cash)} (buffer $${usd(r.cash_buffer_usd)})` : ""; if (!r.n_trades) { out.textContent = `Portfolio already matches target — no trades needed${cashLine}.`; return; } out.innerHTML = `
    ${r.n_trades} trades to match target (min $${usd(r.min_trade_usd)})${cashLine} · dry-run
    ` + r.trades.slice(0, 24).map(t => `
    ${escapeHtml(t.action.toUpperCase())} ${escapeHtml(t.symbol)} ${t.usd>=0?"+":"−"}$${usd(Math.abs(t.usd))}${t.note ? ` ${escapeHtml(t.note)}` : ""}
    `).join("") + `

    Execution runs monthly (or via the token-protected API). This preview places no orders.

    `; } /* ---------- NAV chart ---------- */ async function loadNav() { const d = await fetch("/api/nav").then(r => r.json()).catch(() => null); const cv = $("#navchart"); if (!d || !cv) return; const fund = (d.fund || []).map(p => ({ t: p.t * 1000, v: p.equity })).filter(p => p.v); if (fund.length < 2) { $("#perf-note").textContent = "performance history builds after the first trades"; return; } const spyRaw = (d.spy || []).map(p => ({ t: Date.parse(p.t), v: p.c })); const norm = arr => { const b = arr[0].v; return arr.map(p => ({ t: p.t, v: p.v / b * 100 })); }; const f = norm(fund), s = spyRaw.length > 1 ? norm(spyRaw) : []; const t0 = f[0].t, t1 = f[f.length-1].t; const all = f.concat(s).map(p => p.v); const lo = Math.min(...all) * 0.995, hi = Math.max(...all) * 1.005; const W = cv.clientWidth, H = 240, P = 34; cv.width = W * devicePixelRatio; cv.height = H * devicePixelRatio; cv.style.width = W + "px"; cv.style.height = H + "px"; // display size == coord space (no stretch; exact hover) const g = cv.getContext("2d"); g.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); const X = t => P + (W-P-8) * (t-t0)/(t1-t0||1), Y = v => 12 + (H-12-22) * (1-(v-lo)/(hi-lo||1)); const invX = px => t0 + (px - P) / ((W-P-8)||1) * (t1-t0); const nearest = (arr, t) => arr.reduce((b, p) => Math.abs(p.t-t) < Math.abs(b.t-t) ? p : b, arr[0]); function draw(hoverT) { g.clearRect(0,0,W,H); g.strokeStyle = "#1e2c42"; g.lineWidth = 1; g.font = "10px ui-monospace"; g.fillStyle = "#5d6b83"; [100, (lo+hi)/2, hi, lo].forEach(v => { g.beginPath(); g.moveTo(P, Y(v)); g.lineTo(W-8, Y(v)); g.stroke(); g.fillText(v.toFixed(0), 6, Y(v)+3); }); const line = (arr, col, w) => { if (arr.length<2) return; g.strokeStyle = col; g.lineWidth = w; g.beginPath(); arr.forEach((p,i) => i?g.lineTo(X(p.t),Y(p.v)):g.moveTo(X(p.t),Y(p.v))); g.stroke(); }; line(s, "#8ea3c0", 1.4); line(f, "#37b8a4", 2.2); if (hoverT != null) { const fx = nearest(f, hoverT), sx = s.length ? nearest(s, hoverT) : null; const px = X(fx.t); g.strokeStyle = "#37506e"; g.setLineDash([3,3]); g.beginPath(); g.moveTo(px,10); g.lineTo(px,H-22); g.stroke(); g.setLineDash([]); const dot = (p, col) => { if (!p) return; g.fillStyle = col; g.beginPath(); g.arc(X(p.t), Y(p.v), 3.5, 0, 7); g.fill(); }; dot(sx, "#8ea3c0"); dot(fx, "#37b8a4"); showTip(px, fx, sx); } else hideTip(); } let tip = $("#navtip"); if (!tip) { tip = document.createElement("div"); tip.id = "navtip"; tip.className = "navtip"; cv.parentElement.style.position = "relative"; cv.parentElement.appendChild(tip); } const pct = v => `${v>=100?"+":""}${(v-100).toFixed(2)}%`; function showTip(px, fx, sx) { tip.innerHTML = `
    ${new Date(fx.t).toISOString().slice(0,10)}
    ` + `
    ■ Slipstream ${pct(fx.v)}
    ` + (sx ? `
    ■ SPY ${pct(sx.v)}
    ` : ""); tip.style.display = "block"; tip.style.left = Math.min(Math.max(px + 12, 4), W - 132) + "px"; tip.style.top = "8px"; } function hideTip() { if (tip) tip.style.display = "none"; } draw(); cv.onmousemove = e => { const r = cv.getBoundingClientRect(); const mx = e.clientX - r.left; if (mx < P || mx > W - 8) { draw(); return; } draw(invX(mx)); }; cv.onmouseleave = () => draw(); const last = f[f.length-1].v, sl = s.length ? s[s.length-1].v : null; $("#perf-note").textContent = `since ${new Date(t0).toISOString().slice(0,10)} · Slipstream ${last>=100?"+":""}${(last-100).toFixed(1)}%` + (sl!=null?` vs SPY ${sl>=100?"+":""}${(sl-100).toFixed(1)}%`:""); } /* ---------- picks (no BUY) ---------- */ // The whole category card opens its dedicated top-20 page; interactive children // (refresh button, per-ticker detail links, anchors) keep their own behavior. document.querySelectorAll(".picks").forEach(card => { card.style.cursor = "pointer"; card.addEventListener("click", e => { if (e.target.closest("button, a, .detail-link")) return; location.href = "/picks?panel=" + card.id.replace("panel-", ""); }); }); const panelData = {}; function openDetail(panel, ticker) { const pick = (panelData[panel]?.picks || []).find(p => p.ticker === ticker); if (!pick?.details) return; $("#detail-title").textContent = `${ticker} — ${{superinvestor:"Super Investor Activity",insider:"Insider Activity",news:"News (30 days)"}[panel]||panel}`; $("#detail-body").innerHTML = pick.details.map(sec => `

    ${escapeHtml(sec.title)}

    `).join(""); $("#detail-modal").showModal(); } async function loadPanel(panel, refresh = false) { const body = $(`#panel-${panel} .picks-body`); body.textContent = refresh ? "Refreshing…" : "Loading…"; const resp = await fetch(`/api/picks/${panel}${refresh ? "?refresh=1" : ""}`); if (!resp.ok) { body.innerHTML = `

    ${(await resp.json().catch(()=>({}))).detail || "Unavailable"}

    `; return; } const data = await resp.json(); panelData[panel] = data; const meta = data.as_of ? `

    ${escapeHtml(data.as_of)}${data.disclaimer?" · "+escapeHtml(data.disclaimer):""}

    ` : ""; body.innerHTML = (data.picks.map(p => `
    ${escapeHtml(p.ticker)}
    ${escapeHtml(p.detail)} ${p.llm ? `LLM: ${escapeHtml(p.llm.verdict)}${p.llm.reason?" — "+escapeHtml(p.llm.reason):""}`:""} ${p.details ? `Details ›`:""}
    `).join("") || "

    No qualifying picks.

    ") + meta; body.querySelectorAll(".detail-link").forEach(l => l.addEventListener("click", () => openDetail(l.dataset.panel, l.dataset.ticker))); loadQuotes(data.picks.map(p => p.ticker)); } async function loadQuotes(tickers) { if (!tickers.length) return; const q = await fetch(`/api/quotes?symbols=${tickers.join(",")}`).then(r => r.ok ? r.json() : null).catch(() => null); if (!q) return; for (const [sym, v] of Object.entries(q)) { document.querySelectorAll(`[data-quote="${CSS.escape(sym)}"]`).forEach(el => { const up = v.change_pct != null && v.change_pct >= 0; el.innerHTML = `$${v.price.toFixed(2)}` + (v.change_pct != null ? ` ${up?"▲ +":"▼ "}${v.change_pct}%` : ""); }); } } /* ---------- init ---------- */ $("#detail-close").addEventListener("click", () => $("#detail-modal").close()); $("#detail-modal").addEventListener("click", e => { if (e.target === e.currentTarget) e.currentTarget.close(); }); $("#rebal-preview").addEventListener("click", previewRebalance); document.querySelectorAll(".refresh").forEach(b => b.addEventListener("click", () => loadPanel(b.dataset.panel, true))); /* tabs */ document.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => { document.querySelectorAll(".tab").forEach(x => x.classList.toggle("active", x === t)); document.querySelectorAll(".tab-panel").forEach(p => p.classList.toggle("active", p.id === `tab-${t.dataset.tab}`)); if (t.dataset.tab === "fund") loadNav(); // canvas needs a visible container to size })); loadHealth(); loadStatus(); loadFund(); loadTarget(); loadNav(); ["superinvestor", "insider", "news"].forEach(loadPanel); addEventListener("resize", () => loadNav()); /* ── supervisor health strip (daily flag-only monitor) ───────────────────── */ (async function healthStrip(){ try{ const r = await fetch('/api/supervisor').then(x=>x.json()); const el = document.getElementById('healthstrip'); if(!el || !r.checks) return; const cls = s => s==='OK' ? 'hs-ok' : s==='ALERT' ? 'hs-alert' : 'hs-watch'; const icon = s => s==='OK' ? '●' : s==='ALERT' ? '▲' : '◆'; el.innerHTML = r.checks.map(c => `${icon(c.status)} ${c.name.split(' (')[0]} ${c.status}` ).join('') + `supervisor · ${r.computed_at} · hover for detail`; el.hidden = false; }catch(e){ /* strip is optional */ } })();