Spaces:
Running
Running
Download static/app.js from s-ttp/slipstream: direct link, hf CLI and curl.
- Browser
- Download file 12.6 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
12.6 kB
| /* Slipstream dashboard β the automated paper fund (no manual trading). Picks moved to Signalflow. */ | |
| 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)}%`:""); | |
| } | |
| /* ---------- init ---------- */ | |
| $("#rebal-preview").addEventListener("click", previewRebalance); | |
| /* 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(); | |
| 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 */ } | |
| })(); | |