/* ============================================================ gp-stages.jsx — RADAR (market scan) + SELECTION (asset picks) ============================================================ */ var { useState, useEffect, useMemo } = React; // ---------- RADAR ---------- function RadarView({ onJump }) { const radar = window.GP.DATA.radar; const decisions = window.GP.DATA.decisions; const hasIndicators = radar.hasIndicators; const [sortKey, setSortKey] = useState(hasIndicators ? "adx" : "sym"); const [sortDir, setSortDir] = useState(hasIndicators ? -1 : 1); // selection membership const selectedBy = useMemo(() => { const m = {}; for (const d of decisions) { m[d.asset] = m[d.asset] || []; m[d.asset].push({ strat: d.strategy_id, id: d.id }); } return m; }, [decisions]); // indicator columns only when the snapshot actually carries them (Phase 2) const indicatorCols = hasIndicators ? [ { k: "adx", label: "ADX", w: "0.7fr", align: "right", fmt: (a) => a.adx.toFixed(1) }, { k: "diPlus", label: "+DI", w: "0.7fr", align: "right", fmt: (a) => a.diPlus.toFixed(1), color: () => "var(--emerald)" }, { k: "diMinus", label: "−DI", w: "0.7fr", align: "right", fmt: (a) => a.diMinus.toFixed(1), color: () => "var(--rose)" }, { k: "rsi", label: "RSI", w: "0.7fr", align: "right", fmt: (a) => a.rsi.toFixed(0), color: (a) => a.rsi > 70 ? "var(--rose)" : a.rsi < 32 ? "var(--emerald)" : "var(--fg)" }, { k: "bbPctB", label: "BB %b", w: "0.7fr", align: "right", fmt: (a) => a.bbPctB.toFixed(2) }, { k: "regime", label: "Regime", w: "1.3fr", align: "left" }, ] : []; const cols = [ { k: "sym", label: "Asset", w: "1.4fr", align: "left", get: (a) => a.sym }, { k: "px", label: "Price", w: "1fr", align: "right", fmt: (a) => window.GP.fmt.price(a.px) }, { k: "chg", label: "24h", w: "0.8fr", align: "right", fmt: (a) => window.GP.fmt.signedPct(a.chg), color: (a) => a.chg > 0 ? "var(--emerald)" : "var(--rose)" }, ...indicatorCols, ]; const sorted = useMemo(() => { const arr = [...radar.assets]; arr.sort((a, b) => { const va = a[sortKey], vb = b[sortKey]; if (va == null && vb == null) return 0; if (typeof va === "string" || typeof vb === "string") return sortDir * String(va).localeCompare(String(vb)); return sortDir * (va - vb); }); return arr; }, [sortKey, sortDir, radar.assets]); const toggleSort = (k) => { if (k === sortKey) setSortDir((d) => -d); else { setSortKey(k); setSortDir(-1); } }; const sourceNote = radar.scanned_at ? `universe.json · ${window.GP.fmt.time(radar.scanned_at)}Z · ${window.GP.fmt.ago(radar.scanned_at, window.GP.NOW)} ago${radar.fresh ? "" : " · stale"}` : "no universe snapshot on disk"; return (
Universe scan {radar.count > 0 ? {radar.fresh ? "snapshot fresh" : "snapshot stale"} : no snapshot}
/api/universe · {sourceNote}
{[["assets", radar.count, "var(--fg)"], ["selected", Object.keys(selectedBy).length, "var(--emerald)"]].map(([l, v, c]) => (
{v}
))}
{!hasIndicators && (
⚑ Indicator scan (ADX · RSI · regime · BB %b) is not in the universe artifact — symbols only. Per-asset indicators are a Phase-2 backend serializer change; none are fabricated here.
)} {radar.count === 0 ? (
NO UNIVERSE SNAPSHOT /api/universe returned no assets — the radar scan has not written data/universe.json yet. The committed plan below (Selection / Plan) is unaffected.
) : (
{/* header */}
c.w).join(" ") + " 1.4fr", gap: 10, padding: "12px 8px 8px", borderBottom: "1px solid var(--line)", position: "sticky", top: 0, background: "var(--bg-0)", zIndex: 1 }}> {cols.map((c) => ( ))}
{/* rows */} {sorted.map((a, i) => { const sels = selectedBy[a.sym]; return (
c.w).join(" ") + " 1.4fr", gap: 10, padding: "8px", alignItems: "center", borderBottom: "1px solid var(--line-soft)", background: sels ? "color-mix(in oklab, var(--emerald) 5%, transparent)" : "transparent", }}> {String(i + 1).padStart(2, "0")} {cols.map((c) => { if (c.k === "sym") return (
{a.sym.split("/")[0]} {a.name}
); if (c.k === "regime") return
; return {c.fmt(a)}; })}
{sels ? sels.map((s) => ( )) : }
); })}
)}
); } // ---------- SELECTION ---------- function SelectionView({ onJump }) { const selections = window.GP.DATA.selections; const decisions = window.GP.DATA.decisions; const decByAsset = (strat, sym) => decisions.find((d) => d.strategy_id === strat && d.asset === sym); // derived mode: no client-side fit (symbol-only universe) — picks come from // the engine's committed decisions, not a fabricated fit ranking. const derived = selections.some((s) => s.derived) || selections.every((s) => s.picks.every((p) => p.fit == null)); return (
Asset selection {derived ? "the assets each strategy committed to the D5 sweep, drawn from the live briefing" : "each strategy ranks the scanned universe by regime fit and commits its top picks to the sweep queue"}
{derived && (
⚑ Client-side fit estimate unavailable — the universe snapshot carries symbols only, so per-asset regime fit cannot be recomputed. Picks shown are the engine's committed selections (Phase-2 backend change surfaces fit/rank).
)}
{selections.map((sel) => { const strat = window.GP.STRATEGIES.find((s) => s.id === sel.strategy); if (!strat) return null; const fits = [...sel.picks, ...sel.alternatives].map((p) => p.fit).filter((f) => f != null); const maxFit = fits.length ? Math.max(...fits) : 0; const Row = ({ p, selected }) => { const d = selected ? decByAsset(strat.id, p.sym) : null; const r = d ? window.GP.verify.checkDecision(d) : null; return (
d && onJump(d.id)} style={{ display: "grid", gridTemplateColumns: "24px 1fr 90px auto", gap: 10, alignItems: "center", padding: "9px 10px", borderRadius: 3, cursor: d ? "pointer" : "default", background: selected ? "var(--bg-1)" : "transparent", border: "1px solid " + (selected ? "var(--line)" : "transparent"), opacity: selected ? 1 : 0.55, marginBottom: 4, }}> {p.rank}
{p.sym.split("/")[0]} {r && }
{p.fit != null ? ( <> fit {p.fit.toFixed(3)} ) : ( fit — )}
{d ? sweep → : cut}
); }; return (
{strat.label} {strat.id}
fits {strat.fits} · {sel.scanned ? `scanned ${sel.scanned} · ` : ""}floor {(strat.floor * 100).toFixed(0)}%
{sel.picks.length ? sel.picks.map((p) => ) : no committed picks for this strategy today} {sel.alternatives.length > 0 && ( <>
{sel.alternatives.map((p) => )} )}
); })}
); } Object.assign(window, { RadarView, SelectionView });