/* global React */ /* Vault Control — Strategies screen: registry table, basket groups, deep dive. * Arm/Disarm moved to Basket Studio (2026-07-21) — the basket is where execution is managed; * this screen is the per-strategy record: health, returns, pause/retire. */ const { Card: SgCard } = window.BraveAlphaCapitalDesignSystem_c4b179; const { LineChart: SgLine, MonthBars: SgBars, Sparkline: SgSpark, MeterBar: SgMeter, ModeBars: SgModeBars } = window; const { Stat: SgStat, InfoDot: SgInfo, FMT: SgF, toneOf: sgTone, ModeTag: SgMode, SigTag: SgSig, DecayTag: SgDecay, SourceTag: SgSrc, Msg: SgMsg } = window; const { assetClass: sgAssetClass, ClassChips: SgClassChips, classCounts: sgClassCounts } = window; const { DeployTag: SgDeployTag, depsBySid: sgDepsBySid, venueOf: sgVenueOf } = window; const SG = window.VC; const SG_CLS_LABEL = { crypto: "Crypto", commodities: "Commodities", us_eq: "US Equity" }; function sgRegOf(vault, sid) { return (vault.registry || []).find((r) => r.id === sid) || {}; } // Search ALL accounts (deploymentsAll), not just the vault-scoped list — else an hl-main row is // invisible here and the dialog shows dashes. Prefer the row on the requested account when given. function sgDepOf(vault, sid, accountId) { const all = vault.deploymentsAll || vault.deployments || []; if (accountId) { const m = all.find((d) => d.strategy_id === sid && d.account_id === accountId); if (m) return m; } return all.find((d) => d.strategy_id === sid) || null; } // Small tag for an asset class — used wherever a strategy row appears. function ClassTag({ asset }) { const k = sgAssetClass(asset); const col = k === "crypto" ? "#3E6C8E" : k === "commodities" ? "var(--gold-ink)" : "#5C8A6F"; return {SG_CLS_LABEL[k]}; } // ---- Exit-type classification: Fixed / Non-Fixed / Both --------------------------------- // "Fixed" = exit is pure TP/SL (either a fixed-% risk key in signal_params, e.g. tp_pct/sl_pct, // or exit-logic text that only does entry_price arithmetic, e.g. "close >= entry_price*1.02" — // or no exit info at all, which defaults here to Fixed since that's the shape a native // tp_pct/sl_pct-only strategy takes when no exit-logic string is stored). // "Non-Fixed" = exit is one or more real indicator conditions, no entry_price arithmetic and no // fixed-% risk key anywhere. // "Both" = at least one indicator condition AND a fixed component (entry_price arithmetic // OR'd in, or a risk key present) — the common "indicator OR entry_price*N" pattern. const SG_FIXED_RISK_KEYS = ["hard_stop_pct", "sl_pct", "tp_pct", "sl_roi_pct", "tp_roi_pct", "fixed_bracket", "hard_stop"]; const SG_EXIT_TYPE_LABEL = { fixed: "Fixed", non_fixed: "Non-Fixed", both: "Both" }; function sgExitClauses(logic) { if (logic == null || logic === "False" || logic === false || logic === "") return []; return String(logic).split(/\s+OR\s+/i).map((s) => s.trim()).filter(Boolean); } function sgClassifyExitType(r) { const sp = r.signal_params || {}; const clauses = [ ...sgExitClauses(r.long_exit_logic != null ? r.long_exit_logic : sp.exit_long), ...sgExitClauses(r.short_exit_logic != null ? r.short_exit_logic : sp.exit_short), ...sgExitClauses(sp.exit), ]; const hasPriceClause = clauses.some((c) => /entry_price/i.test(c)); const hasIndicatorClause = clauses.some((c) => !/entry_price/i.test(c)); const hasRiskKey = SG_FIXED_RISK_KEYS.some((k) => sp[k] != null); const hasFixed = hasPriceClause || hasRiskKey; if (hasIndicatorClause && hasFixed) return "both"; if (hasIndicatorClause) return "non_fixed"; return "fixed"; } function ExitTypeTag({ r }) { const k = sgClassifyExitType(r); const col = k === "fixed" ? "#5C8A6F" : k === "non_fixed" ? "#3E6C8E" : "var(--gold-ink)"; const title = k === "fixed" ? "Exit is pure fixed TP/SL (no indicator condition)" : k === "non_fixed" ? "Exit is a dynamic indicator condition (no fixed TP/SL)" : "Exit combines a dynamic indicator condition with a fixed TP/SL"; return {SG_EXIT_TYPE_LABEL[k]}; } // Filter chips for the Type column — same visual language as chrome.jsx's ClassChips, // kept local to this screen since exit-type filtering only makes sense here. const SG_EXIT_TYPES = [["all", "All"], ["fixed", "Fixed"], ["non_fixed", "Non-Fixed"], ["both", "Both"]]; function sgTypeCounts(regs) { return (regs || []).reduce((acc, r) => { const k = sgClassifyExitType(r); acc[k] = (acc[k] || 0) + 1; acc.all = (acc.all || 0) + 1; return acc; }, {}); } function TypeChips({ value, onChange, counts }) { return (
{SG_EXIT_TYPES.map(([id, label]) => { const on = value === id; const n = counts ? (counts[id] || 0) : null; return ( ); })}
); } // ---- Arm / Disarm panel (used from Basket Studio — execution lives with the basket) -------- // One path: DIRECT apply (apply_change) writes live_deployments; the bridge picks it up on // its next ~60s reconcile. Server-side guards (per-sleeve cap, Σalloc≤100%, kill-switch) + the // audit row live in the vault_deploy edge fn — the passcode is the gate. function RequestPanel({ ctx, vault, onClose, onDone }) { // ONE writer per field. This panel writes LIVENESS only (enabled/mode) — never sizing. // The basket owns alloc %/leverage/basket_code and the resolver rewrites them from it on // every ~60s tick, so anything set here would be silently reverted inside a minute. const dep = sgDepOf(vault, ctx.sid, ctx.account_id) || {}; const reg = sgRegOf(vault, ctx.sid) || {}; // Which real account this row lives on — vault_deploy is account-aware, so we send it explicitly. const acctId = ctx.account_id || dep.account_id || null; const venue = acctId ? (sgVenueOf ? sgVenueOf(acctId) : acctId) : ""; const [note, setNote] = React.useState(""); const [msg, setMsg] = React.useState(null); const [busy, setBusy] = React.useState(false); const isArm = ctx.type === "arm"; const isDisarm = ctx.type === "disarm"; const sym = reg.symbol || (reg.asset || "").split("/")[0] || dep.symbol || ""; const inp = { fontFamily: "var(--font-mono)", fontSize: 13.5, padding: "9px 11px", borderRadius: 8, border: "1px solid var(--line)", background: "var(--paper)", color: "var(--ink)", outline: "none", width: "100%", boxSizing: "border-box" }; // What the basket/resolver has already sized this leg at — shown, not edited. const curA = dep.alloc_pct != null ? Number(dep.alloc_pct) * 100 : null; const curL = dep.max_leverage != null ? Number(dep.max_leverage) : null; const pctS = (x) => (x == null ? "—" : x.toFixed(1) + "%"); const levS = (x) => (x == null ? "—" : x + "×"); const sleeveX = (curA != null && curL != null) ? (curA / 100 * curL).toFixed(2) + "x" : "—"; const armWarn = isArm; // arm = real money; always warn // Identity (sid · venue) lives in the title; the summary just says what happens, with real numbers. function summaryLine() { if (isDisarm) return sym + " goes back to Preview — the open position closes within about a minute."; return sym + " goes live at " + pctS(curA) + " × " + levS(curL) + " — " + sleeveX + " exposure. Real orders within ~60s."; } function submit() { setBusy(true); setMsg({ t: "Applying…", err: false }); SG.api("apply_change", { strategy_id: ctx.sid, change_type: ctx.type, account_id: acctId, note: note.trim() || null }) .then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { setMsg({ t: "✓ applied — the bridge picks it up in ≤60s." + (r.body.warning ? " (" + r.body.warning + ")" : ""), err: false }); setTimeout(() => { onClose(); onDone(); }, 1100); return; } setMsg({ t: "Error " + r.status + ": " + (r.body.error || "unknown"), err: true }); }).catch((e) => { setBusy(false); setMsg({ t: "Network error: " + e.message, err: true }); }); } return (
e.stopPropagation()} style={{ width: "min(560px,100%)", background: "var(--surface)", border: "1.5px solid " + (armWarn ? "var(--red)" : "var(--gold)"), borderRadius: 12, padding: "26px 28px", boxShadow: "0 12px 40px rgba(14,30,43,.3)" }}>

{isArm ? "Arm live" : "Disarm"}

{ctx.sid}{venue ? " · " + venue : ""}
{/* The one thing that matters: what this does. Real numbers, sourced from the basket. */}
{summaryLine()}
{isArm && (

Sizing is set by the basket — edit weights & leverage on the basket card.

)} setNote(e.target.value)} style={inp} />
); } function ReqButton({ label, tone: tn, onClick }) { const col = tn === "bad" ? "var(--red)" : "var(--gold-ink)"; return ( ); } // Small chip for the registry lifecycle status — shown wherever the row renders. const SG_STATUS_CFG = { active: { label: "ACTIVE", c: "var(--green)" }, watch: { label: "WATCH", c: "var(--gold-ink)" }, retire: { label: "RETIRED", c: "var(--red)" }, // pre-054 tags: kept so a stale cached payload still renders a labelled chip // rather than silently falling back to ACTIVE. incubation: { label: "INCUBATION", c: "#3d7ea6" }, probation: { label: "PROBATION", c: "var(--gold-ink)" }, shadow: { label: "SHADOW", c: "var(--muted)" }, archived: { label: "ARCHIVED", c: "var(--ink)" }, deprecated: { label: "DEPRECATED", c: "var(--red)" }, }; function StatusChip({ status }) { const cfg = SG_STATUS_CFG[status] || SG_STATUS_CFG.active; return {cfg.label}; } // Lifecycle status control: a two-way toggle between the only two non-terminal tags, // active and watch, plus the terminal Retire button below. Every transition is blocked // server-side while the strategy is armed live on a real account, so the UI stays simple. function LifecycleControl({ sid, status, basketName, onDone }) { const [next, setNext] = React.useState(status || "active"); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(null); // Retire is the ONE-WAY DOOR and it lives in RetireControl below (two-step, 3.5s // auto-reset). It is deliberately NOT in this dropdown: 'archived' used to sit here and // applied on a single click, which is how a strategy could leave the book by accident. // What is left here — active <-> watch — is reversible, so a single Apply is fine. React.useEffect(() => { setNext(status || "active"); }, [status]); if (status === "retire") return null; function apply() { if (next === status) return; // No terminal destination remains in this control, so nothing needs arming here. setBusy(true); setErr(null); SG.api("set_strategy_status", { strategy_id: sid, status: next }).then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { onDone && onDone(); } else setErr(r.body.error || ("HTTP " + r.status)); }).catch((e) => { setBusy(false); setErr(e.message); }); } const changed = next !== status; const options = ["active", "watch"]; return ( {changed && } {err && {err}} ); } // Retire (status → retire; terminal from the UI — the row disappears from Control). // A basket member can't be retired, and instead of a greyed-out button the cell SAYS WHY: // it shows the basket's name, so the reason is readable without hovering. The server // enforces the same rule (409 with the basket name), so this label is honest, not decorative. function RetireControl({ sid, basketName, onDone }) { const [step, setStep] = React.useState(0); // 0 idle · 1 confirm · 2 busy const [err, setErr] = React.useState(null); React.useEffect(() => { if (step !== 1) return; const t = setTimeout(() => setStep(0), 3500); return () => clearTimeout(t); }, [step]); if (basketName) { return ( ); } function click() { if (step === 0) { setStep(1); setErr(null); return; } if (step !== 1) return; setStep(2); SG.api("set_strategy_status", { strategy_id: sid, status: "retire" }) .then((r) => { setStep(0); if (r.status === 200 && r.body.ok) { onDone && onDone(); } else setErr(r.body.error || ("HTTP " + r.status)); }) .catch((e) => { setStep(0); setErr(e.message); }); } const col = step === 1 ? "var(--red)" : "var(--gold-ink)"; return ( {err && {err}} ); } // ---- time-window chips ------------------------------------------------------- const SG_WINDOWS = [["1W", 7], ["1M", 30], ["3M", 90], ["6M", 180], ["All", null]]; // Filter groups on the list header are visually identical chip rows, and both open // with an "All (N)" chip — so side by side they read as one broken control. Give // each the same small caps label the Window control already had. function FilterGroup({ label, children }) { return ( {label} {children} ); } function WinChips({ value, onChange }) { return (
{SG_WINDOWS.map(([lab, d]) => { const on = value === d; return ; })}
); } function sgWinLabel(win) { const hit = SG_WINDOWS.find(([, d]) => d === win); return hit ? hit[0] : "All"; } // Combined return cell: ONE number (backtest + paper + live compounded), red when negative, // plus an explicit labeled sub-line when real live fills exist — never two unlabeled figures. function RetCell({ st, stLive }) { if (!st) return
; const v = st.totalReturn; return (
{(v >= 0 ? "+" : "") + v.toFixed(1) + "%"}
{stLive &&
live {(stLive.totalReturn >= 0 ? "+" : "") + stLive.totalReturn.toFixed(1) + "%"}
}
); } function QuickBasketSetup({ strategy, vault, onDone }) { const [open, setOpen] = React.useState(false); const [basketId, setBasketId] = React.useState(""); const dep = sgDepOf(vault, strategy.id) || {}; const parseLev = (v) => { const m = String(v || "").match(/[\d.]+/); return m ? Number(m[0]) : 0; }; const [alloc, setAlloc] = React.useState(dep.alloc_pct != null ? String(Number(dep.alloc_pct) * 100) : "10"); const lev = dep.max_leverage != null ? Number(dep.max_leverage) : parseLev(strategy.leverage); const [busy, setBusy] = React.useState(false); const [msg, setMsg] = React.useState(null); const baskets = (vault.builderBaskets || []).filter((b) => b.status !== "archived" && !(b.members || []).some((m) => m.strategy_id === strategy.id)); const selectedBasket = baskets.find((b) => String(b.id) === String(basketId)) || null; const currentBasketAlloc = selectedBasket ? (selectedBasket.members || []).reduce((s, m) => s + Number(m.alloc_pct || 0), 0) * 100 : null; const basketAllocAfter = currentBasketAlloc == null ? null : currentBasketAlloc + (Number(alloc) || 0); function save() { const basket = baskets.find((b) => String(b.id) === String(basketId)); const a = Number(alloc), l = Number(lev); if (!basket) { setMsg({ t: "Select a basket.", err: true }); return; } if (!(a > 0 && a <= 100)) { setMsg({ t: "Allocation must be between 0 and 100%.", err: true }); return; } if (!(l > 0)) { setMsg({ t: "No positive leverage is configured in the database.", err: true }); return; } const members = (basket.members || []).map((m) => ({ strategy_id: m.strategy_id, alloc_pct: Number(m.alloc_pct), max_leverage: m.max_leverage == null ? null : Number(m.max_leverage) })); members.push({ strategy_id: strategy.id, alloc_pct: a / 100, max_leverage: l }); if (members.reduce((s, m) => s + Number(m.alloc_pct || 0), 0) > 1.5001) { setMsg({ t: "This would take the basket above its 150% allocation limit.", err: true }); return; } setBusy(true); setMsg({ t: "Adding to basket…", err: false }); SG.api2("update_basket", { id: basket.id, members }).then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { setOpen(false); setMsg(null); onDone && onDone(); } else setMsg({ t: r.body.error || ("HTTP " + r.status), err: true }); }).catch((e) => { setBusy(false); setMsg({ t: e.message, err: true }); }); } return ( <> {open &&
{ if (e.target === e.currentTarget && !busy) setOpen(false); }} style={{ position: "fixed", inset: 0, zIndex: 110, display: "grid", placeItems: "center", padding: 24, background: "rgba(3,7,10,.72)", backdropFilter: "blur(5px)" }}>

Add {strategy.id} to basket

Sizing is prefilled from the strategy or its current deployment.

{selectedBasket &&
Current basket allocation
150.01 ? "var(--red)" : "var(--ink)" }}>{currentBasketAlloc.toFixed(1)}%
After adding
150.01 ? "var(--red)" : basketAllocAfter >= 120 ? "var(--gold-ink)" : "var(--green)" }}>{basketAllocAfter.toFixed(1)}% / 150%
}
Leverage · database
0 ? "var(--ink)" : "var(--red)", fontFamily: "var(--font-mono)" }}>{lev > 0 ? lev + "×" : "Not configured"}
{!baskets.length &&
This strategy is already in every available basket.
}
} ); } // ---- Strategies screen ------------------------------------------------------- function StrategiesList({ vault, mode, openStrategy, refresh }) { const [cls, setCls] = React.useState("all"); const [typ, setTyp] = React.useState("all"); const [query, setQuery] = React.useState(""); const [sort, setSort] = React.useState({ key: null, dir: "desc" }); const [win, setWin] = React.useState(null); // metrics time window (days); null = all history const openPositionSids = new Set( (vault.openTrades || []) .filter((t) => t && t.strategy_id && t.registry_status !== "retire") .map((t) => t.strategy_id) ); // Server-computed health (strategy_health): p_edge / CUSUM / SPRT / drawdown bands, // written by the monitor_decay_regime health job. This is the authoritative decay // read — the client-side computeDecay() below is only a fallback for strategies the // health engine has not reached yet. They disagree because computeDecay() judges a // strategy against a 50% win rate, while the server judges it against its own // BREAKEVEN win rate (p* = 1/(1+PF*(1-wr)/wr)) — a 31%-win / 2.5-PF strategy is // perfectly healthy and the naive check would call it decayed. const healthBySid = {}; (vault.health || []).forEach((h) => { healthBySid[h.strategy_id] = h; }); const SRV_BAND = { ok: "OK", watch: "WATCH", retire: "DECAYED" }; const decayOf = (sid) => { const h = healthBySid[sid]; const sig = (h && h.signals) || null; if (h && h.status && SRV_BAND[h.status]) { return { status: SRV_BAND[h.status], server: true, p_edge: sig && sig.p_edge != null ? sig.p_edge : null, breakeven: sig && sig.breakeven_wr != null ? sig.breakeven_wr : null, n: h.n_live_trades, score: h.health_score, }; } const d = SG.computeDecay(SG.backtestingTradesFor(sid)); return { status: d.status, why: d.why, server: false }; }; // which basket is this strategy in? showcase_baskets is the source of truth. const basketOf = {}; (vault.builderBaskets || []).filter((b) => b.status !== "archived") .forEach((b) => (b.members || []).forEach((m) => { (basketOf[m.strategy_id] = basketOf[m.strategy_id] || []).push(b.name); })); // every in-the-book strategy (retired hidden — retiring is how a row leaves Control) const allRegs = (vault.registry || []).filter((r) => /^id\d+$/.test(r.id || "") && (!r.status || ["active","watch"].includes(r.status))); const q = query.trim().toLowerCase(); const classRegs = cls === "all" ? allRegs : allRegs.filter((r) => sgAssetClass(r.asset) === cls); const typeRegs = typ === "all" ? classRegs : classRegs.filter((r) => sgClassifyExitType(r) === typ); const sortValueOf = (reg, st) => { if (sort.key === "score") { const d = decayOf(reg.id); return d.score != null ? Number(d.score) : (st && st.composite != null ? Number(st.composite) : null); } if (!st) return null; if (sort.key === "return") return st.totalReturn; if (sort.key === "win") return st.winRate; if (sort.key === "composite") return st.composite; if (sort.key === "trades") return st.trades; return null; }; const shownRegs = typeRegs.filter((r) => !q || [r.id, r.name, r.symbol, r.asset] .some((v) => String(v || "").toLowerCase().includes(q))) .slice().sort((a, b) => { if (!sort.key) return 0; const as = SG.lightStatsFor(a.id, win), bs = SG.lightStatsFor(b.id, win); const av = sortValueOf(a, as); const bv = sortValueOf(b, bs); if (av == null && bv == null) return 0; if (av == null) return 1; if (bv == null) return -1; return (av - bv) * (sort.dir === "asc" ? 1 : -1); }); const toggleSort = (key) => setSort((s) => ({ key, dir: s.key === key && s.dir === "desc" ? "asc" : "desc" })); const sortMark = (key) => sort.key === key ? (sort.dir === "asc" ? " ↑" : " ↓") : ""; const winLab = sgWinLabel(win); const grid2 = "minmax(190px,1.5fr) 84px 92px 96px 76px 64px 68px 62px 96px 140px 116px"; // Strategy Class Type Return Win PF Trades Score Decay Basket Actions return (
{/* All strategies — every active + paused registered strategy, all asset classes */}

All strategies

{allRegs.length} in the registry · click any for its full record
setQuery(e.target.value)} placeholder="Search asset or strategy…" aria-label="Search strategies" style={{ width: 230, fontFamily: "var(--font-body)", fontSize: 13, padding: "7px 11px", border: "1px solid var(--line)", borderRadius: 8, background: "var(--paper)", color: "var(--ink)", outline: "none" }} />
StrategyClassType DecayBasketActions
{shownRegs.map((r, i) => { const sid = r.id; const st = SG.lightStatsFor(sid, win); const stLive = SG.statsFor(sid, "live", win); const dec = decayOf(sid); // composite: prefer the server health score (same number Monitor shows), // fall back to the client composite so the column is never blank when // the ledger exists but the health job hasn't run. const score = dec.score != null ? dec.score : (st && st.composite != null ? st.composite : null); return (
{st ? st.winRate.toFixed(0) + "%" : "—"} = 70 ? "var(--green)" : st.composite >= 45 ? "var(--ink)" : "var(--red)") : "var(--muted)" }}>{st && st.composite != null ? Math.round(st.composite) : "—"} {st ? String(st.trades) : "—"} = 70 ? "var(--green)" : score >= 45 ? "var(--ink)" : "var(--red)" }} title={score == null ? "No composite yet — needs a trade ledger or a health-engine run" : (dec.score != null ? "Server composite (health engine): return, Sharpe, profit factor, win rate and drawdown control, weighted" : "Client composite from the trade ledger — health engine has not scored this one yet")}> {score == null ? "—" : Math.round(score)} breakeven); retire below 0.25.") : "Client-side estimate — health engine has not scored this strategy yet"}> {basketOf[sid] ? basketOf[sid].map((name) => {name}) : }
); })} {shownRegs.length === 0 &&
No strategies match this search, asset class, and type.
}
); } // ---- Decay review ----------------------------------------------------------- function DecayReview({ vault, openStrategy, refresh, embedded = false }) { const [decayFilter, setDecayFilter] = React.useState("all"); const registry = {}; (vault.registry || []).forEach((r) => { registry[r.id] = r; }); const basketOf = {}; (vault.builderBaskets || []).filter((b) => b.status !== "archived") .forEach((b) => (b.members || []).forEach((m) => { (basketOf[m.strategy_id] = basketOf[m.strategy_id] || []).push(b.name); })); const openSids = new Set((vault.openTrades || []).map((t) => t.strategy_id)); const healthBySid = {}; (vault.health || []).forEach((h) => { if (h && h.strategy_id) healthBySid[h.strategy_id] = h; }); const rows = (vault.registry || []) .map((r) => ({ r, h: healthBySid[r.id] || { strategy_id: r.id, status: null } })) .filter(({ r, h }) => { const lifecycleRetired = String(r.status || "").toLowerCase() === "retire"; const healthBand = String(h.status || "").toLowerCase(); return lifecycleRetired || ["watch", "retire"].includes(healthBand); }) .sort((a, b) => { // NB: r.status is the REGISTRY tag; h.status is the health band. Both spell one of // their values "retire" since 054 — they are different axes, do not merge them. const rank = (x) => String(x.r.status || "").toLowerCase() === "retire" ? 0 : String(x.h.status).toLowerCase() === "retire" ? 1 : 2; return rank(a) - rank(b) || Number(a.h.health_score ?? 999) - Number(b.h.health_score ?? 999); }); const rowKind = ({ h, r }) => String(r.status || "").toLowerCase() === "retire" ? "retired" : String(h.status).toLowerCase() === "retire" ? "decayed" : "watch"; const retired = rows.filter((row) => rowKind(row) === "retired").length; const decayed = rows.filter((row) => rowKind(row) === "decayed").length; const watch = rows.filter((row) => rowKind(row) === "watch").length; const displayRows = decayFilter === "all" ? rows : rows.filter((row) => rowKind(row) === decayFilter); const grid = "minmax(230px,1.5fr) 92px 84px minmax(210px,1.2fr) minmax(150px,.9fr) 150px"; const evidenceOf = (h) => { const s = h.signals || {}; const out = []; if (s.cusum_alarm) out.push("CUSUM alarm"); if (s.sprt_decision === "retire") out.push("SPRT retire"); if (s.dd_percentile != null) out.push("DD p" + Math.round(Number(s.dd_percentile) * 100)); if (s.p_edge != null) out.push("p_edge " + Number(s.p_edge).toFixed(3)); return out.length ? out.join(" · ") : "Health threshold breached"; }; return (

Decay review

Watch weakening strategies, investigate material decay, and retain the retirement record.

{[["all", "All", rows.length], ["decayed", "Decayed", decayed], ["retired", "Retired", retired], ["watch", "Watch", watch]].map(([key, label, count]) => ( ))}
StrategyDecayScoreEvidenceBasketAction
{displayRows.map(({ h, r }, i) => { const sid = h.strategy_id; const isRetired = String(r.status || "").toLowerCase() === "retire"; const isDecayed = !isRetired && String(h.status).toLowerCase() === "retire"; const isBad = isRetired || isDecayed; const baskets = basketOf[sid] || []; return (
{h.health_score == null ? "—" : Math.round(Number(h.health_score))} {isRetired ? ("Lifecycle " + r.status) : evidenceOf(h)} {baskets.length ? baskets.map((name) => {name}) : } {!isRetired && }
); })} {!displayRows.length &&
No {decayFilter === "all" ? "watch, decayed, or retired" : decayFilter} strategies.
}
); } // ---- Deep dive ----------------------------------------------------------------- const sgBackBtn = { fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: 600, color: "var(--ink)", background: "none", border: "none", borderBottom: "1.5px solid var(--gold)", padding: "0 0 1px", cursor: "pointer", alignSelf: "flex-start", whiteSpace: "nowrap" }; function DeepDive({ sid, vault, mode, openStrategy, back, backLabel, refresh }) { const r = sgRegOf(vault, sid); const dep = sgDepOf(vault, sid); const [ready, setReady] = React.useState(SG.hasRegistryBacktesting(sid)); const [err, setErr] = React.useState(null); const [view, setView] = React.useState("equity"); const [profileMetric, setProfileMetric] = React.useState("return"); const [period, setPeriod] = React.useState("ALL"); const [tradeSide, setTradeSide] = React.useState("ALL"); const [tradeWindow, setTradeWindow] = React.useState("ALL"); const [tradeFrom, setTradeFrom] = React.useState(""); const [tradeTo, setTradeTo] = React.useState(""); const [tradePage, setTradePage] = React.useState(1); const backText = "← " + (backLabel || "Back"); React.useEffect(() => { setTradePage(1); }, [tradeSide, tradeWindow, tradeFrom, tradeTo, sid]); React.useEffect(() => { let on = true; if (SG.hasRegistryBacktesting(sid)) { setReady(true); setErr(null); } else if (!SG.hasRegistryBacktesting(sid)) { setReady(false); setErr(null); SG.ensureRegistryBacktesting([sid]).then(() => on && setReady(true)).catch((e) => on && setErr(e.message)); } else setReady(true); return () => { on = false; }; }, [sid]); if (err) return (
Could not load registry backtesting: {err}
); if (!ready) return (
Loading trade ledger for {sid}…
); const detailTrades = SG.backtestingTradesFor(sid); const allStats = SG.computeStats(detailTrades); if (allStats) allStats.composite = SG.compositeScore(allStats); const periodDays = { "1D": 1, "1W": 7, "1M": 30, "3M": 90, "6M": 180, "1Y": 365 }; const allClosed = allStats ? allStats.ordered : []; const latestClosedMs = allClosed.length ? Math.max(...allClosed.map((t) => SG.parseT(t.exit) || 0)) : 0; const selectedDays = periodDays[period]; // Window buttons use calendar boundaries for monthly P&L and period charts: // 1M means the first through last day of the latest calendar month, 3M means // the first day of the month three months back through the latest month, etc. // 1D/1W are NOT calendar-anchored to the latest trade — they're a true rolling // window from wall-clock now (matching the list page's inWindow() convention), // so a strategy with no recent trades correctly shows "nothing in this window" // instead of a stale window built around whenever it last traded. const calendarStartMs = (() => { if (!selectedDays) return null; if (selectedDays <= 7) return Date.now() - selectedDays * 864e5; if (!latestClosedMs) return null; const d = new Date(latestClosedMs); const monthsBack = selectedDays === 30 ? 0 : selectedDays === 90 ? 2 : selectedDays === 180 ? 5 : 11; return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - monthsBack, 1); })(); const periodTrades = selectedDays ? allClosed.filter((t) => (SG.parseT(t.exit) || 0) >= calendarStartMs) : allClosed; // computeStats(periodTrades) returns null for an empty list — genuinely possible // now that 1D/1W are true rolling windows from now (a strategy with nothing in the // last 24h correctly has zero period trades). Everything below reads s.ordered / // s.winRate / etc. unconditionally, so fall back to a zeroed stats shape rather // than letting a null s crash the page — this renders as an honest "nothing // happened in this window," not stale data and not a blank screen. const EMPTY_STATS = { trades: 0, wins: 0, losses: 0, winRate: 0, pf: 0, avgWin: 0, avgLoss: 0, sharpe: 0, mdd: 0, totalReturn: 0, base100: 100, avgDurH: 0, equity: [100], ordered: [], expectancy: 0, bySrc: {} }; const s = SG.computeStats(periodTrades) || Object.assign({}, EMPTY_STATS); s.composite = SG.compositeScore(s); // Prefer the health engine's verdict over the local heuristic. computeDecay() // compares recent win rate to the first half and judges against 50%; the engine // judges p_edge against this strategy's BREAKEVEN win rate. For a low-win / // high-payoff strategy those two disagree sharply, and the engine is right. const srvH = (vault.health || []).find((h) => h.strategy_id === sid) || null; const SRV_BAND_D = { ok: "OK", watch: "WATCH", retire: "DECAYED" }; const decLocal = SG.computeDecay(periodTrades); const dec = (period === "ALL" && srvH && SRV_BAND_D[srvH.status]) ? Object.assign({}, decLocal, { status: SRV_BAND_D[srvH.status], server: true, signals: srvH.signals || {}, healthScore: srvH.health_score, nLive: srvH.n_live_trades, }) : decLocal; const decayHeadline = dec.status === "OK" ? "No decay detected" : dec.status === "WATCH" ? "Edge is weakening" : dec.status === "DECAYED" ? "Material decay detected" : "Not enough data"; const decaySummary = dec.status === "N/A" ? (dec.why || "More closed trades are required") : dec.dWR >= 0 && dec.dEX >= 0 ? "Recent win consistency and profit per trade are both above the baseline." : dec.dWR < 0 && dec.dEX < 0 ? "Both recent win consistency and profit per trade are below the baseline." : dec.dWR < 0 ? "Win consistency has weakened, while profit per trade remains above baseline." : "Profit per trade has weakened, while win consistency remains above baseline."; const live = dep && (dep.enabled === true || dep.mode === "live"); // which basket holds this strategy — Retire is basket-guarded, and it's useful context const bkName = (() => { let n = null; (vault.builderBaskets || []).filter((b) => b.status !== "archived") .forEach((b) => (b.members || []).forEach((m) => { if (m.strategy_id === sid) n = b.name; })); return n; })(); if (!allStats) return (
No trades recorded for {r.name || sid}.
); const parts = SG.compositeParts(s); const monthly = s.ordered.reduce((acc, t) => { const month = t.exit ? t.exit.slice(0, 7) : null; if (month) acc[month] = (acc[month] || 1) * (1 + t.pnl / 100); return acc; }, {}); Object.keys(monthly).forEach((month) => { monthly[month] = (monthly[month] - 1) * 100; }); const months = Object.keys(monthly).sort(); const perTrade = s.ordered.map((t) => t.pnl); const backtestSpanTrades = s.ordered.filter((t) => t.source === "backtest"); const backtestSpanSource = backtestSpanTrades.length ? backtestSpanTrades : s.ordered; const backtestStartMs = backtestSpanSource.length ? Math.min(...backtestSpanSource.map((t) => SG.parseT(t.entry || t.exit) || Infinity)) : null; const backtestEndMs = backtestSpanSource.length ? Math.max(...backtestSpanSource.map((t) => SG.parseT(t.exit || t.entry) || 0)) : null; const utcDay = (ms) => { const d = new Date(ms); return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); }; const backtestDays = backtestStartMs != null && isFinite(backtestStartMs) && backtestEndMs != null && backtestEndMs > 0 ? Math.max(1, Math.round((utcDay(backtestEndMs) - utcDay(backtestStartMs)) / 864e5) + 1) : 0; const backtestSpanLabel = backtestDays ? `${new Date(backtestStartMs).toISOString().slice(0, 10)} → ${new Date(backtestEndMs).toISOString().slice(0, 10)}` : "no backtest range"; const rollingForPeriod = (win = 10) => { const out = []; for (let i = win - 1; i < s.ordered.length; i++) { const slice = s.ordered.slice(i - win + 1, i + 1); out.push(slice.filter((t) => t.pnl > 0).length / win * 100); } return out; }; const longStats = SG.computeStats(s.ordered.filter((t) => t.type === "LONG")); const shortStats = SG.computeStats(s.ordered.filter((t) => t.type === "SHORT")); const longCount = longStats ? longStats.trades : 0; const shortCount = shortStats ? shortStats.trades : 0; const directionTotal = Math.max(1, longCount + shortCount); const sideShare = (n) => (n / directionTotal) * 100; const rewardRisk = (st) => st && st.losses ? st.avgWin / Math.abs(st.avgLoss) : null; const tradeWindowDays = { "1D": 1, "1W": 7, "1M": 30, "3M": 90, "6M": 180, "1Y": 365 }; const dateInputIso = (value) => { const m = String(value || "").match(/^(\d{2})-(\d{2})-(\d{4})$/); return m ? `${m[3]}-${m[2]}-${m[1]}` : ""; }; const tradeFromIso = dateInputIso(tradeFrom), tradeToIso = dateInputIso(tradeTo); const filteredTrades = allStats.ordered.filter((t) => { if (tradeSide !== "ALL" && t.type !== tradeSide) return false; const exitMs = SG.parseT(t.exit); const days = tradeWindowDays[tradeWindow]; // Rolling from wall-clock now, not from this strategy's last trade — see the // matching fix on the period-return calc above for why (a strategy that hasn't // traded in a month should show an empty 1D/1W trade log, not a stale one). if (days && exitMs < Date.now() - days * 864e5) return false; const exitDay = t.exit ? t.exit.slice(0, 10) : ""; if (tradeFromIso && exitDay < tradeFromIso) return false; if (tradeToIso && exitDay > tradeToIso) return false; return true; }).slice().reverse(); // Live positions for THIS strategy. vault.openTrades is the same feed the Trades // screen uses, already enriched with mark price and unrealised P&L. const openRows = (vault.openTrades || []).filter((t) => t && t.strategy_id === sid && (tradeSide === "ALL" || String(t.type || "").toUpperCase() === tradeSide)); const tradesPerPage = 10; const tradePages = Math.max(1, Math.ceil(filteredTrades.length / tradesPerPage)); const safeTradePage = Math.min(tradePage, tradePages); const pagedTrades = filteredTrades.slice((safeTradePage - 1) * tradesPerPage, safeTradePage * tradesPerPage); const pill = { fontFamily: "var(--font-mono)", fontSize: 12, padding: "3px 10px", borderRadius: 100, background: "var(--paper)", border: "1px solid var(--line)", color: "var(--ink-soft)" }; // Equity-curve annotations: mark where the record changes nature — backtest → paper track, // and the first real broker fill — so "which part of this curve is real" reads off the chart. // (equity[0] = 100; trade k closes at equity index k+1.) const firstPaper = s.ordered.findIndex((t) => t.source !== "backtest"); const firstReal = s.ordered.findIndex((t) => t.source === "live_real"); const markers = []; if (firstPaper > 0) markers.push({ i: firstPaper + 1, label: "PAPER →", color: "#3d7ea6" }); if (firstReal > 0 && firstReal !== firstPaper) markers.push({ i: firstReal + 1, label: "LIVE →", color: "#1F7A4D" }); // Per-source breakdown — the honest split behind the one combined number. const srcStat = (m) => SG.computeStats(s.ordered.filter((t) => m === "backtest" ? t.source === "backtest" : m === "live" ? t.source === "live" : t.source === "live_real")); const stBT = srcStat("backtest"), stLv = srcStat("live"), stRl = srcStat("live_real"); const srcRow = (label, stx, col) => stx && ( {label} {(stx.totalReturn >= 0 ? "+" : "") + stx.totalReturn.toFixed(1) + "%"} {stx.trades}t · {stx.winRate.toFixed(0)}% win ); const pairedStat = (label, left, right, leftColor, rightColor, leftLabel, rightLabel, leftSub, rightSub) => (
{label}
{leftLabel}
{left}
{leftSub &&
{leftSub}
}
{rightLabel}
{right}
{rightSub &&
{rightSub}
}
); const modeProfiles = [ { label: "BOTH", st: s, color: "var(--gold)", share: 100 }, { label: "LONG", st: longStats, color: "var(--green)", share: longStats ? sideShare(longStats.trades) : 0 }, { label: "SHORT", st: shortStats, color: "var(--red)", share: shortStats ? sideShare(shortStats.trades) : 0 }, ]; return (
{r.name || sid} {dep ? : NOT DEPLOYED}
{sid} {r.timeframe && {r.timeframe}}
Composite
{s.composite}
out of 100
{/* The DSL this strategy actually trades. It was previously only visible from Monitor, so the strategy's own page never said what it does. Rendered via window because screens-monitor.jsx loads after this file — resolved at render, and skipped harmlessly if that script ever fails to load. */} {window.StrategyLogicPanel && ( )}
Period
{["ALL", "1D", "1W", "1M", "3M", "6M", "1Y"].map((id) => ( ))}
{pairedStat("Risk efficiency", SgF.num(s.sharpe), SgF.num(s.pf), s.sharpe >= 3 ? "var(--green)" : "var(--ink)", "var(--ink)", "Sharpe (ann.)", "Profit factor", "per-trade × √252", "gross win / gross loss")} {pairedStat("Average outcome", SgF.pct(s.avgWin, 2), SgF.pct(s.avgLoss, 2), "var(--green)", "var(--red)", "Avg win", "Avg loss", "winning trade", "losing trade")} {pairedStat("Trade profile", SgF.dur(s.avgDurH), String(s.trades), "var(--ink)", "var(--ink)", "Avg trade time", "Trades", "entry to exit", "all sources")} {pairedStat("Edge profile", SgF.pct(s.expectancy, 2), rewardRisk(s) != null ? rewardRisk(s).toFixed(2) + "x" : "—", sgTone(s.expectancy), "var(--ink)", "Expectancy", "Reward / risk", "per trade", "gross win / gross loss")}

Long / short profile

Direction mix and independently compounded performance.

{longCount} long · {shortCount} short
{modeProfiles.map(({ label, st, color, share }) => (
{label} {share.toFixed(1)}%
{[["Return", st && SgF.pct(st.totalReturn), st && sgTone(st.totalReturn)], ["Win rate", st && SgF.pctNoSign(st.winRate), "var(--ink)"], ["Profit factor", st && SgF.num(st.pf), "var(--ink)"], ["Expectancy", st && SgF.pct(st.expectancy, 2), st && sgTone(st.expectancy)], ["Reward / risk", rewardRisk(st) != null ? rewardRisk(st).toFixed(2) : "—", "var(--ink)"], ["Drawdown", st && SgF.pctNoSign(st.mdd), "var(--red)" ]].map(([lb, val, tone]) => (
{lb}
{val || "—"}
))}
))}
{profileMetric === "return" ? "Compounded return" : profileMetric === "winRate" ? "Win rate" : "Closed trades"}
{[["return", "Return"], ["winRate", "Win rate"], ["trades", "Trades"]].map(([id, label]) => )}
BOTH 100.0% of trades
{[["Return", SgF.pct(s.totalReturn), sgTone(s.totalReturn)], ["Win rate", SgF.pctNoSign(s.winRate), "var(--ink)"], ["Profit factor", SgF.num(s.pf), "var(--ink)"], ["Expectancy", SgF.pct(s.expectancy, 2), sgTone(s.expectancy)], ["Reward / risk", rewardRisk(s) != null ? rewardRisk(s).toFixed(2) : "—", "var(--ink)"], ["Max drawdown", SgF.pctNoSign(s.mdd), "var(--red)"]].map(([lb, val, tone]) => (
{lb}
{val}
))}
{[["LONG", longStats, "var(--green)"], ["SHORT", shortStats, "var(--red)"]].map(([label, st, color]) => (
{label} {st ? sideShare(st.trades).toFixed(1) : "0.0"}% of trades
{[["Return", st && SgF.pct(st.totalReturn), st && sgTone(st.totalReturn)], ["Win rate", st && SgF.pctNoSign(st.winRate), "var(--ink)"], ["Profit factor", st && SgF.num(st.pf), "var(--ink)"], ["Expectancy", st && SgF.pct(st.expectancy, 2), st && sgTone(st.expectancy)], ["Reward / risk", rewardRisk(st) != null ? rewardRisk(st).toFixed(2) : "—", "var(--ink)"], ["Max drawdown", st && SgF.pctNoSign(st.mdd), "var(--red)"]].map(([lb, val, tone]) => (
{lb}
{val || "—"}
))}
))}

{view === "equity" ? "Account ROI — compounded equity" : view === "rolling" ? "Rolling win rate — 10-trade window" : "Per-trade P&L"}

{[["equity", "Account-ROI"], ["pertrade", "Per-trade"], ["rolling", "Rolling win rate"]].map(([id, lb]) => ( ))}
{/* one curve, provenance annotated ON it — the markers say where backtest ends, the paper track starts, and the first real fill lands */}
{srcRow("BACKTEST", stBT, "var(--muted)")} {srcRow("PAPER", stLv, "#3d7ea6")} {srcRow("LIVE", stRl, "var(--green)")}
{view === "equity" ? ( = 0 ? "var(--green)" : "var(--red)", data: s.equity }]} area accent={s.totalReturn >= 0 ? "var(--green)" : "var(--red)"} baseline={100} height={320} yFmt={(v) => v.toFixed(0)} hover markers={markers} hoverCard={[{ title: "START", value: "100.00", detail: "BASELINE", sub: "account ROI" }].concat(s.ordered.map((t, i) => ({ title: t.exit || "TRADE " + (i + 1), value: s.equity[i + 1] != null ? s.equity[i + 1].toFixed(2) : "—", detail: (t.type || "TRADE") + " · " + String(t.source || "").toUpperCase(), pnl: t.pnl, pnlLabel: (Number(t.pnl) >= 0 ? "+" : "") + Number(t.pnl).toFixed(2) + "%", sub: "held " + (((SG.parseT(t.exit) - SG.parseT(t.entry)) / 3.6e6).toFixed(0)) + "h" })))} /> ) : view === "rolling" ? ( (() => { const roll = rollingForPeriod(10); return roll.length >= 3 ? v.toFixed(0)} xLabels={(i) => "T" + (i + 10)} hover /> :
Rolling win-rate trend appears from ~12 closed trades.
; })() ) : ( (i % 5 === 0 || i === perTrade.length - 1 ? "#" + (i + 1) : ""))} values={perTrade} height={300} /> )}

Monthly P&L

monthly[m] ?? 0)} height={320} barRatio={0.82} />

Composite breakdown

How the {s.composite} is built — each input normalized 0–100, then weighted.

{SG.COMPOSITE_WEIGHTS.map((c) => (
{c.label} max {(c.w * 100).toFixed(0)} pts {(parts[c.key] * c.w * 100).toFixed(1)} / {(c.w * 100).toFixed(0)} pts
))}
Decay check
{dec.status === "N/A" ?
{dec.why || "Insufficient closed trades"}
: (
{[["Win rate", dec.bWR, dec.rWR, dec.dWR, 0], ["Expectancy", dec.bEX, dec.rEX, dec.dEX, 2]].map(([lb, b, rc, dl, dp]) => (
{lb} {b != null ? b.toFixed(dp) : "—"} → {rc != null ? rc.toFixed(dp) : "—"} ({dl != null ? (dl >= 0 ? "+" : "") + dl.toFixed(dp) : "—"})
))} {dec.signals &&
p_edge{dec.signals.p_edge != null ? dec.signals.p_edge.toFixed(3) : "—"}
}
)}

Decay check

{decayHeadline}
{decaySummary}
{dec.status === "N/A" ?
{dec.why || "Insufficient closed trades"}
: (
{[["Win consistency", "Winning trades", dec.bWR, dec.rWR, dec.dWR, 0, " percentage points"], ["Profit per trade", "Average P&L on each trade", dec.bEX, dec.rEX, dec.dEX, 2, "% per trade"]].map(([lb, hint, b, rc, dl, dp, unit]) => { const threshold = lb === "Win consistency" ? -10 : -.75; const col = dl <= threshold ? "var(--red)" : dl < 0 ? "var(--gold-ink)" : "var(--green)"; return (
{lb}{hint} {dl >= 0 ? `Improved by +${dl.toFixed(dp)}${unit}` : `Declined by ${Math.abs(dl).toFixed(dp)}${unit}`}
Baseline
{b.toFixed(dp)}%
Recent
{rc.toFixed(dp)}%
); })}
WATCH at -10pp win rate or -0.75% expectancy. DECAYED at -20pp / -1.5%.
{dec.signals &&
Health confidence · p_edge{dec.signals.p_edge != null ? dec.signals.p_edge.toFixed(3) : "—"}
}
)}
{/* Decay — its own full-width panel: the verdict, the two comparisons behind it, and the rolling win-rate trend so a fading edge is visible as a falling line, not just a chip. */}

Decay check

{dec.status === "N/A" ? (

n/a — {dec.why || "insufficient data"}. Every non-retired strategy is checked on every refresh; this label fills in as closed trades accrue.

) : (
{[["Win rate", dec.bWR, dec.rWR, dec.dWR, "%", 0], ["Expectancy", dec.bEX, dec.rEX, dec.dEX, "%", 2]].map(([lb, b, rc, dl, u, dp]) => { const meterVal = lb === "Win rate" ? rc : (rc != null ? Math.min(100, Math.max(2, (rc + 5) * 10)) : 0); const meterColor = (dl != null && dl < (lb === "Win rate" ? -10 : -0.75)) ? "var(--red)" : "var(--green)"; return (
{lb} {b != null ? b.toFixed(dp) + u : "—"} → {rc != null ? rc.toFixed(dp) + u : "—"} ({dl != null ? (dl >= 0 ? "+" : "") + dl.toFixed(dp) : "—"})
); })} {dec.server && dec.signals ? (
p_edge {dec.signals.p_edge != null ? dec.signals.p_edge.toFixed(3) : "—"}

Verdict is the health engine, not the win-rate delta above: p_edge = P(true win rate > this strategy’s breakeven win rate {dec.signals.breakeven_wr != null ? (dec.signals.breakeven_wr * 100).toFixed(1) + "%" : "—"}) over {dec.nLive != null ? dec.nLive : "?"} live trades, seeded from the backtest baseline. RETIRE below 0.25, WATCH below 0.50. Breakeven — not 50% — is the bar, so a low-win / high-payoff strategy stays healthy at a win rate that would look alarming above. {dec.signals.cusum_alarm ? " CUSUM has fired: the constant-edge assumption just broke." : ""} {dec.signals.sprt_verdict && dec.signals.sprt_verdict !== "undecided" ? " SPRT verdict: " + dec.signals.sprt_verdict + "." : ""}

The Δ bars above are the legacy first-half-vs-recent heuristic, kept for context only.

) : (

baseline = {dec.baseLabel} ({dec.nBase} trades) vs last {dec.nRec} {dec.recentLabel}. WATCH at Δwin ≤ −10pp or Δexp ≤ −0.75%; DECAYED at −20pp / −1.5%.
Health engine has not scored this strategy yet — this is the local estimate.

)}
{(() => { const roll = rollingForPeriod(10); if (roll.length < 3) return
rolling win-rate trend appears from ~12 closed trades
; return (
Rolling win rate — 10-trade window
v.toFixed(0)} xLabels={(i) => "T" + (i + 10)} hover />
); })()}
)}

Trade history

{openRows.length > 0 ? `${openRows.length} open · ` : ""}{filteredTrades.length} closed · 10 per page
Period {["ALL", "1D", "1W", "1M", "3M", "6M", "1Y"].map((id) => ( ))}
Side {["ALL", "LONG", "SHORT"].map((id) => { const active = tradeSide === id; const col = id === "LONG" ? "var(--green)" : id === "SHORT" ? "var(--red)" : "var(--gold)"; return ; })}
Date range
Entry timeExit timeDirectionSourceDurationP&L
{/* Open positions lead the history. The trade ledger holds CLOSED trades only, so without this the page shows a strategy's whole record while staying silent on whether it is in the market right now — an answer that lived only on the Trades screen. Rendered as rows in the same grid rather than a banner so the log reads as one continuous record, newest state first. They are deliberately outside the pager (an open trade has no exit time, so the date/period filters cannot apply to it) but DO honour the direction filter, which is meaningful for them. */} {openRows.map((t, i) => { const isShort = String(t.type || "").toLowerCase() === "short"; const entryMs = SG.parseT(t.entry_time); const heldH = entryMs ? (Date.now() - entryMs) / 3.6e6 : NaN; const pnl = t.unrealized_pnl_pct; const pxAgeMin = t.price_ts ? Math.round((Date.now() / 1000 - Number(t.price_ts)) / 60) : null; return (
{t.entry_time ? String(t.entry_time).slice(0, 16).replace("T", " ") : "—"} OPEN {/* Unrealised P&L is priced from the latest snapshot candle, which can lag — say how old rather than implying a live tick. */} {pxAgeMin != null && pxAgeMin > 20 && ( px {pxAgeMin}m old )} {isShort ? "SHORT" : "LONG"} {isFinite(heldH) ? SgF.dur(heldH) : "—"} {pnl == null ? "—" : (pnl > 0 ? "+" : "") + pnl.toFixed(2) + "%"} {pnl == null ? "—" : (pnl > 0 ? "+" : "") + pnl.toFixed(2) + "%"} {isFinite(heldH) ? SgF.dur(heldH) : "—"}
); })} {pagedTrades.map((t) => { const held = (SG.parseT(t.exit) - SG.parseT(t.entry)) / 3.6e6; return (
{t.entry || "—"} {t.exit || "—"} {t.type} {isFinite(held) ? SgF.dur(held) : "n/a"} {SgF.pct(t.pnl, 2)} {SgF.pct(t.pnl, 2)} {isFinite(held) ? SgF.dur(held) : "—"}
); })} {pagedTrades.length === 0 && openRows.length === 0 &&
No trades match these filters.
}
Page {safeTradePage} of {tradePages} · {filteredTrades.length} trades
); } Object.assign(window, { StrategiesList, DecayReview, DeepDive, RequestPanel, ReqButton, LifecycleControl, RetireControl, StatusChip, ClassTag });