/* ============================================================
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]) => (
))}
{!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) => (
toggleSort(c.k)} style={{
background: "transparent", border: "none", cursor: "pointer", padding: 0,
textAlign: c.align, justifySelf: c.align === "right" ? "end" : "start",
fontFamily: "var(--mono)", fontSize: 9, letterSpacing: "0.12em", textTransform: "uppercase",
color: sortKey === c.k ? "var(--cyan)" : "var(--fg-3)",
}}>{c.label}{sortKey === c.k ? (sortDir < 0 ? " ↓" : " ↑") : ""}
))}
Selected by
{/* 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) => (
onJump(s.id)} style={{ background: "transparent", border: "none", padding: 0, cursor: "pointer" }}>
{s.strat === "MEAN_REV" ? "MEAN-REV" : "TREND"} →
)) :
— }
);
})}
)}
);
}
// ---------- 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 (
fits {strat.fits} · {sel.scanned ? `scanned ${sel.scanned} · ` : ""}floor {(strat.floor * 100).toFixed(0)}%
Committed · {sel.picks.length} {sel.picks.length === 1 ? "pick" : "picks"}
{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 });