Spaces:
Running
Running
Download static/app.js from s-ttp/slipstream: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/s-ttp/slipstream/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/s-ttp/slipstream/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/s-ttp/slipstream/resolve/main/static/app.js
16.2 kB
| /* 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 = `<span class="st muted">Status unavailable — add Alpaca secrets on the Space</span>`; 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 = ` | |
| <span class="st"><i class="dotk ${open ? "open" : "closed"}"></i> Market <b>${open ? "open" : "closed"}</b> · ${next}</span> | |
| <span class="st"><i class="dotk ${done ? "yes" : "no"}"></i> This month: <b>${done ? "rebalanced" : "pending"}</b></span> | |
| <span class="st">Last order: <b>${s.last_order || "none yet"}</b></span> | |
| <span class="st">Auto-rebalance: <b>${s.rebalance_enabled ? "on" : "off (set REBALANCE_TOKEN)"}</b> · monthly, 1st trading day</span>`; | |
| } | |
| /* ---------- fund ---------- */ | |
| async function loadFund() { | |
| const resp = await fetch("/api/fund"); | |
| const hold = $("#holdings"); | |
| if (!resp.ok) { hold.innerHTML = `<p class="error">${(await resp.json().catch(()=>({}))).detail || "Fund unavailable — add Alpaca secrets"}</p>`; 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]) => `<div class="kpi"><div class="kpi-l">${l}</div><div class="kpi-v ${c}">${v}</div></div>`).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 ? `<table><thead><tr> | |
| <th>Symbol</th><th>Weight</th><th>Qty</th><th>Price</th><th>P/L %</th><th>Value</th></tr></thead><tbody> | |
| ${p.positions.map(x => `<tr> | |
| <td class="sym">${escapeHtml(x.symbol)}<div class="wbar"><i style="width:${(x.weight/maxw*100).toFixed(0)}%"></i></div></td> | |
| <td>${(x.weight*100).toFixed(1)}%</td><td>${x.qty.toFixed(2)}</td><td>$${x.current.toFixed(2)}</td> | |
| <td class="${x.pl_pct>=0?"pl-pos":"pl-neg"}">${x.pl_pct>=0?"+":""}${x.pl_pct}%</td> | |
| <td>$${usd(x.market_value)}</td></tr>`).join("")}</tbody></table>` | |
| : "<p class='muted'>No open positions yet — run a rebalance to build the book.</p>"; | |
| } | |
| /* ---------- 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 => `<div class="tgt-row"> | |
| <span class="t-sym">${escapeHtml(h.ticker)}</span><span class="t-sec">${escapeHtml(h.sector)}</span> | |
| <span class="t-w">${(h.weight*100).toFixed(1)}%</span></div>`).join("") | |
| + `<p class="as-of">${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)}` : "") | |
| + `<br>${escapeHtml(t.rule)}</p>`; | |
| 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 | |
| ? `<span class="rs-hit">${ex.length} excluded</span> · ${rs.cleared}/${rs.scanned} candidates cleared` | |
| : `<span class="rs-ok">all clear</span> · ${rs.scanned} candidates scanned, no material adverse events`; | |
| const li = e => `<li><span class="rs-sev rs-${escapeHtml(e.severity)}">${escapeHtml(e.severity)}</span> | |
| <b>${escapeHtml(e.ticker)}</b> — ${escapeHtml(e.event)} <span class="rs-cat">${escapeHtml(e.category)}</span> | |
| ${e.headline ? `<span class="rs-src">“${escapeHtml(e.headline)}”</span>` : ""}</li>`; | |
| const rows = ex.map(li).join(""); | |
| const med = (rs.flags_medium || []); | |
| const medRows = med.map(li).join(""); | |
| el.innerHTML = `<div class="rs-head">🛡 AI Risk Screen — ${head}</div>` + | |
| (rows ? `<ul class="rs-list">${rows}</ul>` : "") + | |
| (medRows ? `<div class="rs-sub">Monitoring (below exclusion threshold — held):</div><ul class="rs-list">${medRows}</ul>` : "") + | |
| `<p class="rs-note">A second-stage AI overlay scans the final candidates for <b>concrete, recent adverse events</b> — | |
| legal · regulatory/FDA · geopolitical/trade · operational · competitive — and hard-excludes only <b>critical</b> 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.</p>`; | |
| } | |
| 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 = `<span class="error">${r?.detail || "unavailable"}</span>`; 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 = `<div class="muted">${r.n_trades} trades to match target (min $${usd(r.min_trade_usd)})${cashLine} · dry-run</div>` | |
| + r.trades.slice(0, 24).map(t => `<div class="trade"><span class="t-${escapeHtml(t.action)}">${escapeHtml(t.action.toUpperCase())} ${escapeHtml(t.symbol)}</span> | |
| <span>${t.usd>=0?"+":"−"}$${usd(Math.abs(t.usd))}${t.note ? ` <small class="muted">${escapeHtml(t.note)}</small>` : ""}</span></div>`).join("") | |
| + `<p class="as-of">Execution runs monthly (or via the token-protected API). This preview places no orders.</p>`; | |
| } | |
| /* ---------- 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 = `<div class="nt-date">${new Date(fx.t).toISOString().slice(0,10)}</div>` + | |
| `<div class="nt-row"><span style="color:#37b8a4">■</span> Slipstream <b>${pct(fx.v)}</b></div>` + | |
| (sx ? `<div class="nt-row"><span style="color:#8ea3c0">■</span> SPY <b>${pct(sx.v)}</b></div>` : ""); | |
| 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 => `<section class="detail-section"><h4>${escapeHtml(sec.title)}</h4> | |
| <ul>${sec.items.map(it => `<li>${it.href?`<a href="${escapeHtml(it.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(it.text)} ↗</a>`:escapeHtml(it.text)}</li>`).join("")}</ul></section>`).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 = `<p class="error">${(await resp.json().catch(()=>({}))).detail || "Unavailable"}</p>`; return; } | |
| const data = await resp.json(); panelData[panel] = data; | |
| const meta = data.as_of ? `<p class="as-of${data.stale ? " stale" : ""}">${escapeHtml(data.as_of)}${data.disclaimer?" · "+escapeHtml(data.disclaimer):""}</p>` : ""; | |
| body.innerHTML = (data.picks.map(p => `<div class="pick"> | |
| <div class="pick-top"><span class="pick-ticker">${escapeHtml(p.ticker)}</span><span class="quote" data-quote="${escapeHtml(p.ticker)}"></span></div> | |
| <span class="pick-detail">${escapeHtml(p.detail)}</span> | |
| ${p.llm ? `<span class="pick-llm ${escapeHtml((p.llm.verdict||"unverified").toLowerCase())}">LLM: ${escapeHtml(p.llm.verdict)}${p.llm.reason?" — "+escapeHtml(p.llm.reason):""}</span>`:""} | |
| ${p.details ? `<a class="detail-link" data-panel="${escapeHtml(panel)}" data-ticker="${escapeHtml(p.ticker)}">Details ›</a>`:""} | |
| </div>`).join("") || "<p class='muted'>No qualifying picks.</p>") + 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 ? ` <span class="${up?"q-up":"q-down"}">${up?"▲ +":"▼ "}${v.change_pct}%</span>` : ""); | |
| }); | |
| } | |
| } | |
| /* ---------- 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 => | |
| `<span class="hs-chip ${cls(c.status)}" title="${(c.detail||'').replace(/"/g,'"')}">${icon(c.status)} ${c.name.split(' (')[0]} <b>${c.status}</b></span>` | |
| ).join('') + `<span class="hs-note">supervisor · ${r.computed_at} · hover for detail</span>`; | |
| el.hidden = false; | |
| }catch(e){ /* strip is optional */ } | |
| })(); | |