/* global React */
/* Vault Control — Trade Log + Ops (open positions, live positions, orders, alerts). */
const { Card: TlCard } = window.BraveAlphaCapitalDesignSystem_c4b179;
const { FMT: TlF, toneOf: tlTone, SourceTag: TlSrc, InfoDot: TlInfo, depsBySid: tlDepsBySid, venueOf: tlVenueOf } = window;
const { SearchSelect: TlSearchSelect } = window;
const TL = window.VC;
function tlReg(vault, sid) { return (vault.registry || []).find((r) => r.id === sid) || {}; }
// ---- Open positions — every strategy (moved here from Strategies, 2026-07-21) --------
// One open row per strategy with an open trade, priced from the latest candle. Vocabulary
// matches SourceTag everywhere: LIVE = armed real money; PAPER = the evaluator's paper track.
function OpenTradesTable({ vault, openStrategy, bare }) {
const depMapAll = tlDepsBySid(vault.deploymentsAll || vault.deployments || []);
const isSimAcct = (a) => (tlVenueOf ? tlVenueOf(a) : a) === "PAPER" || a === "paper_main" || a === "hyperliquid_testnet";
const basketOf = {};
(vault.builderBaskets || []).filter((b) => b.status !== "archived")
.forEach((b) => (b.members || []).forEach((m) => { basketOf[m.strategy_id] = b.name; }));
// retired strategies keep their ledger but their dangling opens are noise, not positions
const open = (vault.openTrades || []).filter((t) => t.registry_status !== "retire");
if (!open.length) return bare ?
no open positions
: null;
const bySym = {};
open.forEach((t) => { const s = t.symbol || (tlReg(vault, t.strategy_id).symbol || "?"); (bySym[s] = bySym[s] || []).push(t); });
const syms = Object.keys(bySym).sort();
const chipOf = (sid) => {
const d = depMapAll[sid];
const liveReal = d && (d.enabled === true || d.mode === "live") && !isSimAcct(d.account_id);
if (liveReal) return { t: "LIVE", c: "var(--green)", tip: "Armed — real money on " + (tlVenueOf ? tlVenueOf(d.account_id) : d.account_id) };
if (basketOf[sid]) return { t: "PAPER", c: "#3d7ea6", tip: "In basket “" + basketOf[sid] + "” — not armed; this is the evaluator's paper track" };
return { t: "PAPER", c: "#3d7ea6", tip: "Registry strategy — evaluator paper track only" };
};
const pctTone = (v) => (v == null ? "var(--muted)" : tlTone(v));
const gridOP = "minmax(170px,1.4fr) 74px 110px 110px 90px 100px 110px";
const Wrap = bare ? "div" : TlCard;
return (
{!bare && (
Open positions — every strategy
Every open position across all strategies, grouped by asset and priced from the latest candle.
Only LIVE rows are real money — PAPER is the evaluator running the same signals without money.
);
}
// ---- Positions: two tabs, Live (real broker positions, every account) first ----
function PositionsTabs({ vault, refresh, openStrategy }) {
const [tab, setTab] = React.useState("live");
return (
{[["live", "Live positions"], ["open", "Open positions — every strategy"]].map(([id, lb]) => (
))}
{tab === "live" ? "broker positions · every connected account" : "every strategy's open trade · LIVE = money, PAPER = evaluator"}
{tab === "live"
?
: }
);
}
function SideTag({ side }) {
const s = String(side || "").toLowerCase();
const long = s === "long" || s === "buy";
return {s.toUpperCase()};
}
// ---- manual close of a live position (Control → Supabase → GCP reconciler) -------
// Two-step confirm. Calls close_trade: books the exit + writes a manual_closes suppression
// marker so the reconciler flattens the broker position and the evaluator won't re-open it.
function CloseCell({ sid, orphan, refresh }) {
const [st, setSt] = React.useState(null);
if (!sid) return —;
if (st === "done") return closing…;
if (st === "busy") return …;
if (st && st.err) return error ⚠;
function go() {
setSt("busy");
window.VC.api("close_trade", { strategy_id: sid, note: "manual close from Trade Log" }).then((r) => {
if (r.status === 200 && r.body.ok) { setSt("done"); setTimeout(() => refresh && refresh(), 1400); }
else setSt({ err: (r.body && r.body.error) || ("HTTP " + r.status) });
}).catch((e) => setSt({ err: e.message }));
}
if (st === "confirm") return (
);
// Orphan = the position outlived its deployment (disarmed while still open). Same
// action, but flagged gold so the operator knows nothing is managing this line and
// it will sit there until someone closes it.
const tone = orphan ? "var(--gold)" : "var(--red)";
return ;
}
// ---- Ops: live positions -----------------------------------------------------------
// Recent orders used to sit beside this in a 2-col grid; it now renders full-width lower down
// (see RecentOrders) — it needs the width for its filters, and it is reference, not cockpit.
function OpsTables({ vault, refresh }) {
// Collapse duplicate rows for the same economic position. A HIP-3 builder-dex position
// carries a dex prefix ("xyz:AMD") while a pre-switch bare row ("AMD") can linger in the
// live_positions cache until the position closes (reconcile only purges the legacy bare
// twin on close). Key by (account, base symbol), keep the freshest row, so the cockpit
// shows one live line — never a stale duplicate.
const canonSym = (s) => String(s || "").replace(/^[a-z0-9]+:/i, "").toUpperCase();
const tsOf = (s) => Date.parse(String(s || "").replace(" ", "T")) || 0;
const posByKey = {};
(vault.positions || []).forEach((p) => {
const k = String(p.account_id || TL.VAULT) + "|" + canonSym(p.symbol);
if (!posByKey[k] || tsOf(p.updated_at) > tsOf(posByKey[k].updated_at)) posByKey[k] = p;
});
const pos = Object.values(posByKey).sort((a, b) =>
(String(a.account_id) + "|" + canonSym(a.symbol)).localeCompare(String(b.account_id) + "|" + canonSym(b.symbol)));
// (account, base symbol) -> owning strategy, so a position's Close writes THAT account's
// strategy — symbol alone is ambiguous once positions span every broker account, and the
// deployment symbol is bare ("AMD") while the exchange position may be prefixed ("xyz:AMD").
//
// This used to key on `d.enabled` ALONE, which silently hid Close on any position whose
// strategy had been disarmed. Disarm (apply_change) only flips enabled/mode — it never
// flattens — so a live position routinely outlives its deployment, and the one control
// that could close it disappeared exactly when it was needed (SUI/hyperliquid_vault,
// 2026-09-07: -$52 short, both SUI deployments in shadow, no Close button, no owner).
// Rank instead: an armed deployment wins, then one that actually holds the open trade,
// so a disarmed-but-open sleeve still gets a (gold) Close.
const openSids = {}; (vault.openTrades || []).forEach((t) => { openSids[t.strategy_id] = true; });
const depRank = (d) => (d.enabled ? 2 : 0) + (openSids[d.strategy_id] ? 1 : 0);
const depByKey = {}; (vault.deploymentsAll || vault.deployments || []).forEach((d) => {
const k = String(d.account_id || TL.VAULT) + "|" + canonSym(d.symbol);
if (!depByKey[k] || depRank(d) > depRank(depByKey[k])) depByKey[k] = d;
});
const th = { fontSize: 11, fontWeight: 600, letterSpacing: ".05em", textTransform: "uppercase", color: "var(--muted)" };
const grid = "68px 84px 60px 1fr 1fr 1fr 82px 44px 70px 76px";
return (
SymbolAccountSideQtyEntryMarkuPnLLevUpdatedClose
{pos.map((p, i) => {
const up = Number(p.unrealized_pnl);
const dep = depByKey[String(p.account_id || TL.VAULT) + "|" + canonSym(p.symbol)];
return (