/* global React */ /* Vault Control — chrome: gate, sidebar, top bar, info popovers, shared tags. */ const { Tag } = window.BraveAlphaCapitalDesignSystem_c4b179; const VC = window.VC; // ---- Lucide icon helper ------------------------------------------------------ function Icon({ name, size = 18, color = "currentColor", style = {} }) { const ref = React.useRef(null); React.useEffect(() => { if (window.lucide && ref.current) { ref.current.innerHTML = ""; const el = document.createElement("i"); el.setAttribute("data-lucide", name); ref.current.appendChild(el); window.lucide.createIcons({ attrs: { width: size, height: size, stroke: color, "stroke-width": 1.75 }, nameAttr: "data-lucide" }); } }, [name, size, color]); return ; } // Overview was cut (2026-07-21, operator request — it repeated Basket Studio / Trade Log); // Compare folded into Analytics the same day. Basket Studio is the landing screen. const NAV = [ ["baskets", "Basket Studio", "shapes"], ["environments", "Broker Accounts", "shield"], ["strategies", "Strategies", "list-checks"], ["analytics", "Analytics", "line-chart"], ["monitor", "Monitor", "activity"], ["trades", "Trade Log", "receipt-text"], ]; // ---- formatting helpers -------------------------------------------------------- const F = { pct: (v, dp = 1) => (v >= 0 ? "+" : "−") + Math.abs(v).toFixed(dp) + "%", pctNoSign: (v, dp = 1) => v.toFixed(dp) + "%", num: (v, dp = 2) => (v === Infinity ? "∞" : v.toFixed(dp)), dur: (h) => (h >= 48 ? (h / 24).toFixed(1) + " d" : h.toFixed(0) + " h"), money: (v) => (v == null || !isFinite(Number(v)) ? "—" : "$" + Number(v).toLocaleString("en-US", { maximumFractionDigits: 2 })), x: (v, dp = 2) => Number(v || 0).toFixed(dp) + "×", }; function tone(v) { return v > 0 ? "var(--green)" : v < 0 ? "var(--red)" : "var(--ink)"; } // ---- Info popovers ------------------------------------------------------------- const INFO = { vault: { t: "What is Vault Control", b: "Vault = the Hyperliquid vault — the single real-money account every strategy here trades inside. This page is the only governed door into it. Each strategy runs in its own isolated-margin bucket: its allocation % of equity is its margin, its leverage applies to that bucket alone.", d: "Think per-strategy, not per-account. A 10× strategy next to a 2× strategy is fine — they don't share risk." }, persleevecap: { t: "Per-strategy cap", b: "The most market exposure any single strategy may reach = alloc% × leverage. Isolated margin means this is judged per strategy, never summed across the book.", d: "This is why one strategy can be 3× and another 10× at the same time." }, account: { t: "Account status", b: "The vault's broker_accounts state. KILL SWITCH = the bridge halts and flattens everything; nothing trades until it's cleared.", d: "If you ever see KILL SWITCH, no orders are going out — that's a stop-the-world state, not a per-strategy pause." }, heartbeats: { t: "System heartbeats", b: "Last run of the execution bridge (should be every minute) and the strategy evaluator (every ~15 min). Green <5m, amber <30m, red beyond.", d: "A red bridge means orders are NOT being reconciled — armed positions are drifting from their targets. Escalate." }, regime: { t: "Market regime", b: "The latest trend/volatility read per asset from the monitor's regime detector. Empty until the monitor writes it.", d: "A mean-reversion strategy in a strong trend, or a breakout strategy in chop, is fighting its regime." }, basketsov: { t: "Why this is a table, not one number", b: "Isolated margin means risk doesn't sum cleanly across the whole account — you manage exposure per basket. Each row is one basket's own margin, market exposure and decay, computed only from that basket's strategies.", d: "Read this table basket-by-basket. The combined figure above is a secondary total, not the primary number." }, systemstrip: { t: "System — account-wide only", b: "These four are genuinely account-level: the per-strategy cap is one constant, account status and heartbeats are singular, and the combined total is a sum shown for reference.", d: "For anything you'd act on, look at the Basket risk table, not this strip." }, combinedtotal: { t: "All baskets, combined", b: "Σ margin and Σ market exposure across every basket's armed strategies. A reference total only — it can look fine while one basket is over-extended and another is empty.", d: "Don't manage off this number. Use it as a sanity check after the per-basket table." }, basketmargin: { t: "Margin used (this basket)", b: "Sum of allocation % across this basket's armed strategies only. This basket's own share of the 100% equity budget.", d: "A basket near its ceiling is a reason to size new strategies into a different basket." }, basketnotional: { t: "Market exposure (this basket)", b: "Σ(alloc × lev) over this basket's armed strategies — its market exposure as a multiple of total vault equity.", d: "High market exposure in one basket means it alone is doing a lot of the account's market-sensitivity." }, basketdecay: { t: "Decay (this basket)", b: "Count of this basket's strategies in each decay state — green = OK, amber = WATCH, red = DECAYED, grey = not enough trades yet.", d: "A basket with a red dot has a strategy whose live performance fell off its baseline — open Strategies to find which." }, strategy: { t: "Strategy — one per asset", b: "Each row is an isolated-margin bucket shown as ASSET · strategy id. The rule is strict: one strategy per asset.", d: "To change which strategy trades an asset, replace the existing one." }, basket: { t: "Basket", b: "The basket this strategy belongs to. Strategies are grouped into baskets; each strategy has an allocation (its weight) inside its basket. A basket is the unit you deploy to a broker account.", d: "Manage baskets in Basket Studio, and how much of each account goes to each basket in Broker Accounts." }, mode: { t: "Live vs preview", b: "LIVE = trading: the system places real orders for it. Preview = the system computes the intended position but places nothing.", d: "New strategies start in Preview. Arming makes it Live — a passcode-protected, checked, logged change; real orders follow within ~60s." }, alloc: { t: "Strategy allocation %", b: "The share of the broker account's capital assigned to this strategy. All armed allocations on an account together stay ≤ 100%.", d: "This is your position-sizing lever — how big this strategy runs." }, lev: { t: "Leverage ×", b: "Leverage applied inside this strategy only (isolated). One strategy at 10× does not raise any other strategy's risk. Constraint: alloc × lev ≤ the per-strategy cap.", d: "Use leverage for conviction on a single strategy without touching the rest of the book." }, sleevex: { t: "Market exposure ×", b: "alloc × lev — this strategy's market exposure as a multiple of total vault equity.", d: "Compare across strategies to see which carry the most exposure per unit of equity." }, stage: { t: "Staging a draft", b: "Shadow rows stage instantly — the change writes as shadow/disabled, safe. LIVE rows apply DIRECTLY: staging one writes the vault and the bridge reconciles it into real orders within ~60s. There is no SQL step.", d: "Edit freely in the draft — nothing moves until you stage. Staging a LIVE row is a real-money change: guarded (per-strategy cap, Σalloc ≤ 100%, kill-switch) and audited, but immediate." }, deployments: { t: "Vault deployments", b: "Every strategy on the vault with its live signal, health and analytics. Open a strategy for its full record: equity curve, rolling win rate, decay breakdown, trade log.", d: "This is your per-strategy cockpit — open a row before any arm/disarm decision." }, deploymentenv: { t: "Broker account", b: "One broker account — its API keys (stored encrypted) and how much of the account goes to each basket. This is the 'Broker Account API' step: the account that actually places the trades. One per Hyperliquid Vault, Hyperliquid Main, or a future exchange.", d: "Set which baskets this account runs and what share of its capital each gets. A strategy's live size = this account's basket allocation × the strategy's allocation inside that basket." }, signal: { t: "Current signal", b: "The evaluator's current desired position for this strategy: LONG, SHORT, or FLAT (no position).", d: "This is what the bridge is trying to hold right now if the strategy is armed." }, decaycol: { t: "Decay status", b: "Is recent live performance holding up vs the backtest baseline? OK / WATCH / DECAYED. WATCH at Δwin ≤ −10pp or Δexpectancy ≤ −0.75%; DECAYED at −20pp / −1.5%.", d: "DECAYED means the strategy has materially diverged from its validated baseline and should be reviewed for retirement." }, exittypecol: { t: "Exit type", b: "Fixed = the exit is pure take-profit/stop-loss (a fixed % or price-based rule, no indicator condition). Non-Fixed = the exit is a dynamic indicator condition, no fixed TP/SL. Both = the exit combines a dynamic indicator condition with a fixed TP/SL (whichever fires first).", d: "Derived from the strategy's stored exit logic and risk parameters — not a separate stored field." }, actions: { t: "Actions", b: "Preview strategies offer Arm (go live). Live strategies offer Disarm (return to Preview). Sizing is set on the basket, not here.", d: "Applies directly: the bridge picks it up on its next ~60s reconcile." }, draftalloc: { t: "Live execution — direct edit", b: "Edits the executing positions directly (the older path). The source of truth is the basket: edit a basket's strategies and weights on the basket itself, and the account's basket split in Broker Accounts.", d: "Kept until the resolver keeps execution in sync from the baskets automatically. Prefer editing the basket." }, lifecycle: { t: "Lifecycle status", b: "The strategy's place in the book — three tags: active (in the book, tradeable and seatable), watch (degraded but alive: still tracked and signalled, blocked from new basket seats), retire (out of the book: no new seats, though an open trade can still close).", d: "The health engine recommends active↔watch and auto-applies watch→retire after 90 days without recovery; Control applies the money-touching changes." }, healthcol: { t: "Health score", b: "0–100 composite of p_edge, CUSUM drift, drawdown percentile and regime fit. OK ≥70, WATCH 40–70, RETIRE <40.", d: "Crosses between bands trigger health alerts; the score itself is shown here for scanability." }, recommend: { t: "Lifecycle recommendation", b: "What the health engine thinks the next status should be. Money-touching transitions (active↔watch) are recommendations for a human; only watch→retire is auto-applied.", d: "Click through to the strategy detail and use the status control to apply a recommendation." }, addsleeve: { t: "Add a shadow strategy", b: "Bring a registry strategy onto the vault as a new shadow strategy. Blocked if that asset already has a strategy (one-per-asset).", d: "New strategies always land shadow/disabled — safe to add and study before ever arming." }, comparepick: { t: "Compare strategies", b: "Pick 2\u20136 strategies — deployed or not, any registry strategy with trade history. Their ledgers load on demand.", d: "Use it before building a basket: compare candidates for the same asset." }, correlation: { t: "Weekly-return correlation", b: "Pearson correlation of the strategies' weekly PnL (inactive weeks counted as 0), needs ≥8 overlapping weeks. Green = low/negative (diversifying), red = high (redundant).", d: "Two red-correlated strategies are largely the same bet — size accordingly." }, }; function InfoDot({ k }) { const [open, setOpen] = React.useState(false); const [pos, setPos] = React.useState(null); const btnRef = React.useRef(null); const d = INFO[k]; if (!d) return null; function toggle(e) { e.preventDefault(); e.stopPropagation(); if (open) { setOpen(false); return; } const r = btnRef.current.getBoundingClientRect(); const pw = Math.min(340, window.innerWidth - 24); const left = Math.max(12, Math.min(r.left + r.width / 2 - 30, window.innerWidth - pw - 12)); const below = r.bottom + 190 < window.innerHeight; setPos({ left, top: below ? r.bottom + 8 : null, bottom: below ? null : window.innerHeight - r.top + 8, width: pw }); setOpen(true); } return ( {open && ( setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 60 }} /> {d.t} {d.b} {d.d && → {d.d}} )} ); } // ---- small shared tags ----------------------------------------------------------- function ModeTag({ live }) { return live ? LIVE : PREVIEW; } function SigTag({ sig }) { const s = ["long", "short", "flat"].indexOf(sig) >= 0 ? sig : "flat"; const col = s === "long" ? "var(--green)" : s === "short" ? "var(--red)" : "var(--muted)"; return {s.toUpperCase()}; } function DecayTag({ status }) { const col = status === "OK" ? "var(--green)" : status === "WATCH" ? "var(--gold-ink)" : ["DECAYED", "RETIRED"].includes(status) ? "var(--red)" : "var(--muted)"; return {status}; } // ONE vocabulary for trade provenance, everywhere: BACKTEST = historical simulation; // PAPER = the evaluator's forward paper track (ledger source 'live'); LIVE = a real broker // fill (ledger source 'live_real'). The old BT/LIVE/REAL labels made "real" ambiguous. function SourceTag({ src }) { const lb = src === "backtest" ? "BACKTEST" : src === "live_real" ? "LIVE" : "PAPER"; const col = src === "backtest" ? "var(--muted)" : src === "live_real" ? "var(--green)" : "#3d7ea6"; const bg = src === "backtest" ? "var(--paper)" : src === "live_real" ? "var(--green-soft)" : "rgba(61,126,166,.09)"; const bd = src === "backtest" ? "var(--line)" : col; return {lb}; } // Venue + state for a live_deployments row, across EVERY account — not just the vault. // account_id is the only venue signal on the wire: broker_accounts.broker would discriminate // paper more robustly, but list_vault scopes that read to ALLOWED_ACCOUNT, so it never arrives. // Do NOT switch this to broker_mode — paper_main.broker_mode is 'vault', same as the real vault. const VENUES = { hyperliquid_vault: "VAULT", hyperliquid_main: "HL MAIN", hyperliquid_testnet: "TESTNET", paper_main: "PAPER", breakout_main: "BREAKOUT", }; // Every OTHER account is named by its own display_name, so adding a broker never // needs an edit here. Control fills this in from list_environments; until that // lands (or for an account with no environment row) venueOf derives a label from // the slug itself. VENUES above stays authoritative so the two original // Hyperliquid accounts keep the short names the desk already reads. const VENUE_LABELS = {}; function registerVenueLabels(envs) { (envs || []).forEach((e) => { const id = String(e.slug || ""); const name = String(e.display_name || "").trim(); if (id && name && !VENUES[id]) VENUE_LABELS[id] = name.toUpperCase(); }); } function venueOf(accountId) { if (!accountId) return "VAULT"; // legacy rows predate account_id const id = String(accountId); // slugify() emits hyphens, so strip those as well as underscores — otherwise a // slug-derived label reads as "PRARABDH-COINBASE" beside VAULT and HL MAIN. return VENUES[id] || VENUE_LABELS[id] || id.replace(/[_-]/g, " ").toUpperCase(); } // One strategy can be deployed on SEVERAL accounts at once — id107 TSLA is on paper_main AND // hyperliquid_main right now. list_vault returns live_deployments unordered, so a last-wins map // would flip that tag between PAPER and HL MAIN across refreshes. Rank by how real the venue is // (and prefer a live row over a shadow one on the same venue) so the tag is deterministic. const VENUE_RANK = { hyperliquid_vault: 0, hyperliquid_main: 1, breakout_main: 2, hyperliquid_testnet: 3, paper_main: 4 }; function venueRank(d) { const r = VENUE_RANK[String(d.account_id || "")]; // Unknown venue (any broker added since): below the named real ones, above paper. // Every unknown account shared the same 2.5, so two of them tied and the tag could // flip across refreshes — the exact non-determinism this function exists to stop. // A tiny stable offset from the id keeps ties resolved the same way every time. const idHash = String(d.account_id || "").split("").reduce((h, c) => (h * 31 + c.charCodeAt(0)) % 997, 7); const base = r != null ? r : (d.account_id ? 2.5 + idHash / 100000 : 0); return base * 2 + ((d.enabled === true || d.mode === "live") ? 0 : 1); } function depsBySid(list) { const out = {}; (list || []).forEach((d) => { const cur = out[d.strategy_id]; if (!cur || venueRank(d) < venueRank(cur)) out[d.strategy_id] = d; }); return out; } // LIVE mirrors the backend's own liveness guard (vault_deploy: enabled === true || mode === 'live'). function deployTagOf(dep) { if (!dep) return { t: "REGISTRY", c: "var(--muted)", bd: "var(--line)" }; const venue = venueOf(dep.account_id); if (String(dep.account_id || "") === "paper_main") return { t: venue, c: "#3d7ea6", bd: "#3d7ea6" }; const live = dep.enabled === true || dep.mode === "live"; return live ? { t: venue + " · LIVE", c: "var(--green)", bd: "var(--green)" } : { t: venue + " · PREVIEW", c: "var(--gold-ink)", bd: "var(--gold)" }; } function DeployTag({ dep }) { const d = deployTagOf(dep); return {d.t}; } function Stat({ label, value, color, sub, size = 24, info }) { return (
{label}{info && } {value} {sub && {sub}}
); } function Msg({ text, err }) { if (!text) return null; return
{text}
; } // ---- Asset-class filter (shared by Compare + Showcase pickers) -------------------- // Taxonomy mirrors the basket structure: crypto (/USDT), commodities (GOLD/SILVER), // else US equity. Lets an operator scope a strategy picker to one asset class. const ASSET_CLASSES = [["all", "All"], ["crypto", "Crypto"], ["commodities", "Commodities"], ["us_eq", "US Equity"]]; function assetClass(asset) { const a = String(asset || "").toUpperCase(); const b = a.split("/")[0]; if (b === "GOLD" || b === "SILVER") return "commodities"; if (a.indexOf("USDT") >= 0) return "crypto"; return "us_eq"; } function classCounts(regs) { return (regs || []).reduce((acc, r) => { const k = assetClass(r.asset); acc[k] = (acc[k] || 0) + 1; acc.all = (acc.all || 0) + 1; return acc; }, {}); } function ClassChips({ value, onChange, counts }) { return (
{ASSET_CLASSES.map(([id, label]) => { const on = value === id; const n = counts ? (counts[id] || 0) : null; return ( ); })}
); } // ---- Gate -------------------------------------------------------------------------- function Gate({ onUnlocked }) { const [code, setCode] = React.useState(() => VC.cachedPasscode ? VC.cachedPasscode() : ""); const [msg, setMsg] = React.useState(null); const [busy, setBusy] = React.useState(false); const triedCache = React.useRef(false); React.useEffect(() => { if (triedCache.current || VC.DEMO || !code) return; triedCache.current = true; unlock(code, true); }, []); function unlock(nextCode, fromCache) { const candidate = nextCode == null ? code : nextCode; if (!VC.DEMO && !candidate) { setMsg({ t: "Enter the passcode.", err: true }); return; } VC.setPasscode(candidate || null); setBusy(true); setMsg({ t: "Unlocking…", err: false }); VC.api("list_vault").then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { if (!VC.DEMO && VC.cachePasscode) VC.cachePasscode(candidate); onUnlocked(r.body); return; } VC.clearSession(); if (r.status === 503) setMsg({ t: "Backend not activated yet — the VAULT_DEPLOY_PASSCODE secret is not set on the server.", err: true }); else if (r.status === 401) { if (VC.forgetPasscode) VC.forgetPasscode(); if (fromCache) setCode(""); setMsg({ t: fromCache ? "Saved passcode expired or changed. Enter it again." : "Wrong passcode.", err: true }); } else setMsg({ t: "Error " + r.status + ": " + (r.body.error || "unknown"), err: true }); }).catch((e) => { setBusy(false); VC.clearSession(); setMsg({ t: "Network error: " + e.message, err: true }); }); } return (
HEDGEDBAC
Internal VAULT CONTROL

Strategy & basket manager.

The governed door into the Hyperliquid vault — baskets, strategies and live execution, over the live trade record.

setCode(e.target.value)} placeholder="Saved in this browser after unlock" onKeyDown={(e) => e.key === "Enter" && unlock()} style={{ width: "100%", boxSizing: "border-box", fontFamily: "var(--font-mono)", fontSize: 14, padding: "11px 13px", borderRadius: 8, border: "1px solid var(--line)", background: "var(--paper)", color: "var(--ink)", outline: "none" }} />

Live control — arm, edit or disarm a strategy and it takes effect on the broker account within about a minute (checked and logged). New strategies start in preview.

HedgedBAC internal

); } // ---- Sidebar ------------------------------------------------------------------------- function SidebarLegacy({ route, go, vault, onRefresh, onLock, refreshing }) { const account = (vault && vault.account) || {}; const kill = !!account.kill_switch; const [collapsed, setCollapsed] = React.useState(() => { try { return window.localStorage.getItem("hedgedbac.control.v2.sidebar") === "collapsed"; } catch (_) { return false; } }); // Dark mode: a data-theme attribute on , persisted per browser. index.html sets it // before first paint so there is no flash; this toggle just flips + stores it. const [theme, setTheme] = React.useState(() => document.documentElement.dataset.theme || "light"); function toggleCollapsed() { const next = !collapsed; setCollapsed(next); try { window.localStorage.setItem("hedgedbac.control.v2.sidebar", next ? "collapsed" : "expanded"); } catch (_) { } } function flipTheme() { const next = theme === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; try { window.localStorage.setItem("hedgedbac.control.v2.theme", next); } catch (_) { } setTheme(next); } return ( ); } function Sidebar({ route, go, vault, onRefresh, onLock, refreshing }) { const account = (vault && vault.account) || {}; const kill = !!account.kill_switch; const [collapsed, setCollapsed] = React.useState(() => { try { return window.localStorage.getItem("hedgedbac.control.v2.sidebar") === "collapsed"; } catch (_) { return false; } }); const [theme, setTheme] = React.useState(() => document.documentElement.dataset.theme || "light"); function toggleCollapsed() { const next = !collapsed; setCollapsed(next); try { window.localStorage.setItem("hedgedbac.control.v2.sidebar", next ? "collapsed" : "expanded"); } catch (_) { } } function flipTheme() { const next = theme === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; try { window.localStorage.setItem("hedgedbac.control.v2.theme", next); } catch (_) { } setTheme(next); } const utilityButton = { minHeight: 34, display: "flex", alignItems: "center", justifyContent: "center", gap: 7, fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600, padding: "7px 0", borderRadius: 7, border: "1px solid var(--ink-line)", background: "transparent", cursor: "pointer", }; return ( ); } // ---- Source toggle + top bar ------------------------------------------------------- function SourceToggle({ mode, setMode }) { const opts = [["backtest", "Backtest"], ["live", "Live"], ["both", "Both"]]; return (
Source
{opts.map(([id, label]) => ( ))}
); } // Deliberately bare: no account tag, no "as of", no source toggle (all cut 2026-07-21 — // operator request; provenance now reads off SourceTags and chart markers instead). function TopBar({ title, sub, right }) { return (

{title}

{sub &&

{sub}

}
{right &&
{right}
}
); } Object.assign(window, { Icon, InfoDot, Gate, Sidebar, TopBar, SourceToggle, ModeTag, SigTag, DecayTag, SourceTag, DeployTag, deployTagOf, venueOf, registerVenueLabels, depsBySid, Stat, Msg, FMT: F, toneOf: tone, VC_NAV: NAV, assetClass, ASSET_CLASSES, ClassChips, classCounts });