// Vantage — Build 1: Portfolio Manager.
// Every element answers a question a market participant actually has:
// what am I worth, what moved today, am I beating the market, and where
// am I concentrated. Quotes can be missing (market data down) — rows then
// show cost basis and say so, never a fabricated price.

function vgPositionRow(p, q) {
  const price = q?.price ?? null;
  const prev = q?.prevClose ?? null;
  const value = price != null ? p.shares * price : null;
  const cost = p.shares * p.cost_basis;
  return {
    ...p,
    price, prev,
    name: q?.name ?? null,
    value, cost,
    day$: price != null && prev != null ? p.shares * (price - prev) : null,
    dayPct: price != null && prev != null && prev !== 0 ? price / prev - 1 : null,
    pnl$: value != null ? value - cost : null,
    pnlPct: value != null && cost !== 0 ? value / cost - 1 : null,
  };
}

function VgSummary({ rows, bench }) {
  const priced = rows.filter(r => r.value != null);
  const value = priced.reduce((a, r) => a + r.value, 0);
  const costPriced = priced.reduce((a, r) => a + r.cost, 0);
  const costAll = rows.reduce((a, r) => a + r.cost, 0);
  const day = priced.some(r => r.day$ != null) ? priced.reduce((a, r) => a + (r.day$ ?? 0), 0) : null;
  const prevValue = value - (day ?? 0);
  const pnl = priced.length ? value - costPriced : null;
  const cells = [
    { l: 'total value', v: vgMoney(priced.length ? value : null), sub: priced.length < rows.length ? `${rows.length - priced.length} unquoted · cost ${vgMoney(costAll)}` : `cost ${vgMoney(costAll)}`, c: VG.ink },
    { l: 'day change', v: vgMoney(day), sub: day != null && prevValue > 0 ? vgPct(day / prevValue) : '—', c: vgDelta(day) },
    { l: 'total p&l', v: vgMoney(pnl), sub: pnl != null && costPriced > 0 ? vgPct(pnl / costPriced) : '—', c: vgDelta(pnl) },
    {
      l: 'vs s&p 500', v: bench ? vgPct(bench.port - bench.spx) : '—',
      sub: bench ? `you ${vgPct(bench.port)} · spx ${vgPct(bench.spx)} · ${bench.range}` : 'needs market data',
      c: bench ? vgDelta(bench.port - bench.spx) : VG.ink3,
    },
  ];
  return (
    <section style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 10, marginBottom: 14 }}>
      {cells.map(c => (
        <div key={c.l} style={{ background: VG.tile, borderRadius: 18, padding: '14px 18px' }}>
          <div style={vgS.caps}>{c.l}</div>
          <div style={{ ...vgS.serif, ...vgS.num, fontSize: 27, color: c.c, marginTop: 4, lineHeight: 1.05 }}>{c.v}</div>
          <div style={{ ...vgS.num, fontSize: 11.5, color: VG.ink3, marginTop: 3 }}>{c.sub}</div>
        </div>
      ))}
    </section>
  );
}

// The bench (growth of invested dollar vs S&P) is computed server-side by
// the same pure function the risk board's drawdown uses — accountSeries in
// vantage-market.js — so the chart and the risk numbers can never disagree.

// ── The risk board ────────────────────────────────────────────────────────

// Monte Carlo cone: p10/median/p90 paths of the book's own resampled daily
// returns. Levels start at 1; the dashed rail marks breakeven.
function VgCone({ cone, w = 560, h = 120 }) {
  if (!cone?.length) return null;
  const pad = 6;
  const lo = Math.min(...cone.map(c => c.p10), 1), hi = Math.max(...cone.map(c => c.p90), 1);
  const x = i => pad + (i / (cone.length - 1)) * (w - 2 * pad);
  const y = v => h - pad - ((v - lo) / (hi - lo || 1)) * (h - 2 * pad);
  const line = k => cone.map((c, i) => `${x(i)},${y(c[k])}`).join(' ');
  const band = [
    ...cone.map((c, i) => `${x(i)},${y(c.p90)}`),
    ...[...cone].reverse().map((c, i) => `${x(cone.length - 1 - i)},${y(c.p10)}`),
  ].join(' ');
  return (
    <svg viewBox={`0 0 ${w} ${h}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
      <polygon points={band} fill={VG.accent + '16'} />
      <line x1={pad} x2={w - pad} y1={y(1)} y2={y(1)} stroke={VG.rule} strokeDasharray="3 4" />
      <polyline points={line('p90')} fill="none" stroke={VG.up} strokeWidth="1" strokeDasharray="4 3" opacity="0.7" />
      <polyline points={line('median')} fill="none" stroke={VG.accent} strokeWidth="1.8" />
      <polyline points={line('p10')} fill="none" stroke={VG.down} strokeWidth="1" strokeDasharray="4 3" opacity="0.7" />
    </svg>
  );
}

// Pairwise correlation grid. Accent intensity = how positively two names
// move together; negative pairs (the diversifying kind) print plain.
function VgCorrGrid({ corr }) {
  if (!corr) return null;
  const { symbols, matrix } = corr;
  const cell = { width: 44, height: 30, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 6, fontSize: 10.5 };
  return (
    <div style={{ overflowX: 'auto' }}>
      <div style={{ display: 'inline-grid', gridTemplateColumns: `56px repeat(${symbols.length}, 46px)`, gap: 2 }}>
        <div />
        {symbols.map(s => <div key={s} style={{ ...vgS.caps, textAlign: 'center', alignSelf: 'end', paddingBottom: 2 }}>{s}</div>)}
        {symbols.map((rowSym, i) => (
          <React.Fragment key={rowSym}>
            <div style={{ ...vgS.caps, alignSelf: 'center' }}>{rowSym}</div>
            {symbols.map((colSym, j) => {
              const r = matrix[i][j];
              const alpha = r != null && r > 0 ? Math.round(r * 160).toString(16).padStart(2, '0') : '00';
              return (
                <div key={colSym} style={{
                  ...cell, ...vgS.num,
                  background: i === j ? VG.chip : r != null && r > 0 ? VG.accent + alpha : VG.chip,
                  color: i === j ? VG.ink4 : r != null && r > 0.6 ? '#fff' : VG.ink2,
                }}>
                  {r == null ? '—' : i === j ? '·' : r.toFixed(2)}
                </div>
              );
            })}
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

// The portfolio as one measured object: correlation, beta, vol, Sharpe/
// Sortino, drawdown, and the bootstrap cone — all on "today's book, held
// through the window" (current weights). The account's money-weighted
// drawdown lives under the bench chart, labeled as such. Every number
// carries its window and n; missing market data degrades to a sentence.
function VgRisk({ positions, earnings = {} }) {
  const [win, setWin] = React.useState('1y');
  const [risk, setRisk] = React.useState(null);        // null loading, undefined unreachable
  const [moves, setMoves] = React.useState({});
  const [macro, setMacro] = React.useState(null);      // regime context for the beta join
  const [expanded, setExpanded] = React.useState(false); // corr matrix + MC cone are opt-in detail
  // Risk math reads symbols + shares; expected move depends on the symbol
  // set only, so a share edit must not refire the heavy options-chain fetch.
  const riskKey = positions.map(p => p.symbol + p.shares).join(',');
  const symsOnly = [...new Set(positions.map(p => p.symbol))].filter(s => !s.startsWith('^')).sort().join(',');
  React.useEffect(() => {
    if (!positions.length) return;
    let alive = true;
    setRisk(null);
    vgGet(`/vantage/api/risk?range=${win}`)
      .then(d => { if (alive) setRisk(d); })
      .catch(() => { if (alive) setRisk(undefined); });
    return () => { alive = false; };
  }, [riskKey, win]);
  React.useEffect(() => {
    if (!symsOnly) return;
    vgGet(`/vantage/api/expectedmove?symbols=${symsOnly}`)
      .then(d => setMoves(d.moves || {})).catch(() => {});
  }, [symsOnly]);
  // Macro regime, joined to the book's beta below — the two heaviest
  // quantitative objects in the app finally meet.
  React.useEffect(() => {
    vgGet('/vantage/api/macro').then(setMacro).catch(() => {});
  }, []);
  if (!positions.length) return null;

  const stats = risk?.stats;
  const mc = risk?.montecarlo;
  const stat = (l, v, sub, c) => (
    <div style={{ background: VG.chip, borderRadius: 12, padding: '10px 12px', minWidth: 0 }}>
      <div style={vgS.caps}>{l}</div>
      <div style={{ ...vgS.serif, ...vgS.num, fontSize: 21, color: c || VG.ink, marginTop: 2 }}>{v}</div>
      {sub && <div style={{ fontSize: 10.5, color: VG.ink4, marginTop: 2 }}>{sub}</div>}
    </div>
  );
  const emRows = Object.values(moves);

  return (
    <section style={{ background: VG.tile, borderRadius: 18, padding: '15px 18px', marginBottom: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
        <span style={vgS.caps}>risk board · today's book, held through the window</span>
        <span style={{ flex: 1 }} />
        {stats && (risk.corr || mc) && (
          <button onClick={() => setExpanded(e => !e)} style={{
            appearance: 'none', border: 0, cursor: 'pointer', background: VG.chip,
            color: VG.ink3, borderRadius: 999, padding: '3px 12px', fontSize: 11,
          }}>{expanded ? 'hide detail ▲' : 'show detail ▾'}</button>
        )}
        <VgRangeToggle options={['6mo', '1y', '2y']} value={win} onChange={setWin} />
      </div>

      {risk === null && <div style={{ ...vgS.serif, color: VG.ink3, fontSize: 14 }}>measuring the book…</div>}
      {risk === undefined && <div style={{ color: VG.ink3, fontSize: 12.5 }}>risk service unreachable — the board returns when it does.</div>}
      {risk?.error && <div style={{ color: VG.ink3, fontSize: 12.5 }}>{risk.error}</div>}
      {risk?.empty && <div style={{ color: VG.ink3, fontSize: 12.5 }}>add positions and the book becomes measurable.</div>}

      {stats && (
        <React.Fragment>
          {risk.excluded?.length > 0 && (
            <div style={{ fontSize: 12, color: '#fb923c', marginBottom: 6 }}>
              ▲ excluding {risk.excluded.join(', ')} — no market data right now. The numbers below are NOT your full book.
            </div>
          )}
          {risk.readout && <VgBlurb text={risk.readout.note} />}
          {(() => {
            // Macro regime × book beta: only speaks when the regime is actually
            // stressed AND the book's sensitivity is measured — silence otherwise.
            if (risk.beta == null || !macro) return null;
            const bits = [];
            if (['inverted', 'flat'].includes(macro.curve?.band?.key)) bits.push(`curve ${macro.curve.band.key}`);
            if (macro.hyOas?.band?.key && macro.hyOas.band.key !== 'benign') bits.push(`credit ${macro.hyOas.band.key}`);
            if (!bits.length) return null;
            return (
              <div style={{ fontSize: 12, color: '#fb923c', marginTop: 8, lineHeight: 1.5 }}>
                ◆ macro regime: {bits.join(' · ')} — at beta {vgNum(risk.beta)}, index-level stress reaches this book at roughly {vgNum(risk.beta)}× strength.
              </div>
            );
          })()}

          {(() => {
            const ci = stats.sharpeCI;
            const uncertain = ci && ci[0] <= 0 && ci[1] >= 0;
            return (
              <React.Fragment>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(118px, 1fr))', gap: 8, margin: '10px 0 12px' }}>
                  {stat('beta vs s&p', vgNum(risk.beta), 'book sensitivity to the index')}
                  {stat('ann. volatility', vgPct(stats.annVol, false), `${stats.n} trading days`)}
                  {stat('sharpe', vgNum(stats.sharpe),
                    ci ? `95% CI ${vgNum(ci[0])}–${vgNum(ci[1])}` : (stats.rfAnnual > 0 ? `rf ${(stats.rfAnnual * 100).toFixed(1)}% (^IRX)` : 'raw ratio'),
                    uncertain ? VG.ink3 : vgDelta(stats.sharpe))}
                  {stat('sortino', vgNum(stats.sortino), 'downside-only vol', uncertain ? VG.ink3 : vgDelta(stats.sortino))}
                  {stat('max drawdown', vgPct(risk.bookMaxDrawdown, false), 'this book, this window', risk.bookMaxDrawdown < -0.15 ? VG.down : undefined)}
                </div>
                {uncertain && (
                  <div style={{ fontSize: 11.5, color: VG.ink3, margin: '-4px 0 12px', lineHeight: 1.45 }}>
                    at {stats.n} trading days, Sharpe's 95% interval spans zero — read the risk-adjusted ratios as directional, not a settled edge.
                  </div>
                )}
              </React.Fragment>
            );
          })()}

          {expanded && risk.corr && (
            <div style={{ marginBottom: 12 }}>
              {/* Gradual introduction: one pair is a single number, not a
                  structure — the grid earns its place at three names. */}
              {(risk.corr.symbols?.length ?? 0) >= 3 ? (
                <React.Fragment>
                  <div style={{ ...vgS.caps, marginBottom: 6 }}>pairwise correlation · daily returns · n {risk.corr.n}</div>
                  <VgCorrGrid corr={risk.corr} />
                </React.Fragment>
              ) : (
                <div style={{ fontSize: 11.5, color: VG.ink4 }}>
                  {vgFeatureIntro({ holdingCount: risk.corr.symbols?.length ?? 0 }).corrGrid.hint}
                </div>
              )}
            </div>
          )}

          {expanded && mc && (
            <div style={{ marginBottom: 10 }}>
              <div style={{ ...vgS.caps, marginBottom: 6 }}>one-year cone · bootstrap of this book's own returns</div>
              <VgCone cone={mc.cone} />
              <div style={{ display: 'flex', gap: 14, marginTop: 6, fontSize: 11.5, flexWrap: 'wrap' }}>
                <span style={{ color: VG.accent }}>median {vgPct(mc.terminal.median)}</span>
                <span style={{ color: VG.down }}>p10 {vgPct(mc.terminal.p10)}</span>
                <span style={{ color: VG.up }}>p90 {vgPct(mc.terminal.p90)}</span>
                <span style={{ color: VG.ink2 }}>chance of a 20% drawdown along the way: <b style={{ ...vgS.num, color: mc.probMaxDrawdown.dd20 > 0.3 ? VG.down : VG.ink }}>{Math.round(mc.probMaxDrawdown.dd20 * 100)}%</b></span>
              </div>
              <div style={{ ...vgS.serif, fontSize: 12.5, color: VG.accent2, marginTop: 7, lineHeight: 1.45 }}>
                {mc.paths} paths resampled from {mc.n} real days of this book in ~{mc.blockLen}-day blocks — no distribution assumed.
                Clustering survives only partly, so treat the drawdown odds as a floor, not a ceiling.
              </div>
            </div>
          )}

          {emRows.length > 0 && (
            <div>
              <div style={{ ...vgS.caps, marginBottom: 6 }}>expected move · options-implied vs the name's own realized</div>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                {emRows.map(m => {
                  const eDays = earnings?.[m.symbol]?.date ? Math.ceil((earnings[m.symbol].date - Date.now()) / 86400000) : null;
                  const intoEarnings = eDays != null && eDays >= 0 && m.daysToExpiry != null && eDays <= m.daysToExpiry + 2;
                  const rcColor = m.richCheap === 'rich' ? VG.accent2 : m.richCheap === 'cheap' ? VG.up : VG.ink4;
                  // Dollar-ize against the actual position: ±% × (shares × spot).
                  const heldShares = positions.filter(p => p.symbol === m.symbol).reduce((a, p) => a + Number(p.shares), 0);
                  const posValue = m.spot != null && heldShares > 0 ? heldShares * m.spot : null;
                  const dollarMove = posValue != null ? posValue * m.movePct : null;
                  return (
                    <span key={m.symbol} style={{ background: VG.chip, borderRadius: 999, padding: '5px 12px', fontSize: 11.5 }}
                      title={posValue != null ? `${vgNum(heldShares, 4)} sh × ${vgMoney(m.spot)} = ${vgMoney(posValue)} at risk of the move` : undefined}>
                      <b style={{ color: VG.ink }}>{m.symbol}</b>
                      <span style={{ ...vgS.num, color: VG.ink2 }}> ±{(m.movePct * 100).toFixed(1)}%</span>
                      {dollarMove != null && (
                        <span style={{ ...vgS.num, color: VG.ink }}> (±{vgMoney(dollarMove)} of your {vgMoney(posValue)})</span>
                      )}
                      {m.richCheap && m.realizedPct != null && (
                        <span style={{ color: rcColor }}
                          title={`options-implied ±${(m.movePct * 100).toFixed(1)}% vs its realized ±${(m.realizedPct * 100).toFixed(1)}% over a comparable span`}>
                          {' '}· {m.richCheap} vs ±{(m.realizedPct * 100).toFixed(1)}% real
                        </span>
                      )}
                      {/* expiry is epoch ms; vgDate wants an ISO date string */}
                      {intoEarnings
                        ? <span style={{ color: '#fb923c' }}> · into earnings in {eDays}d</span>
                        : <span style={{ color: VG.ink4 }}> by {m.expiry != null ? vgDate(new Date(m.expiry).toISOString()) : '—'}</span>}
                    </span>
                  );
                })}
              </div>
            </div>
          )}

          <div style={{ fontSize: 10.5, color: VG.ink4, marginTop: 10 }}>
            window {risk.window.from} → {risk.window.to} · {risk.window.tradingDays} trading days
            {risk.window.limitedBy ? ` · limited by ${risk.window.limitedBy}'s history` : ''}
          </div>
        </React.Fragment>
      )}
    </section>
  );
}

// ── Paper desk: ghost trades against real price data ──────────────────────
// A separate ledger from the manual holdings above: place market orders at
// the app's honest price basis (live quote or last owned EOD close, labeled),
// slippage modeled against you, book derived server-side by pure math.
function VgPaperDesk({ quotes }) {
  const [state, setState] = React.useState(null);          // null loading, undefined unreachable
  const [f, setF] = React.useState({ symbol: '', side: 'buy', qty: '' });
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [open, setOpen] = React.useState(true);
  const [liveQuote, setLiveQuote] = React.useState(null);  // fallback for symbols not in the app quote map
  const [chartSym, setChartSym] = React.useState('SPY');   // the TradingView pane follows the ticker input (debounced)

  const load = React.useCallback(() => {
    vgGet('/vantage/api/paper').then(setState).catch(() => setState(undefined));
  }, []);
  React.useEffect(load, [load]);

  const sym = f.symbol.toUpperCase().trim();
  // Preview any symbol, not just held ones — fetch a quote when it isn't already
  // in the app's quote map (mirrors the decision form), so the price and modeled
  // slippage show before you place a ghost trade on something you don't own yet.
  React.useEffect(() => {
    setLiveQuote(null);
    if (!sym || quotes[sym] || !/^[A-Z0-9.^-]{1,12}$/.test(sym)) return;
    let alive = true;
    const t = setTimeout(() => {
      vgGet(`/vantage/api/quotes?symbols=${encodeURIComponent(sym)}`)
        .then(d => { if (alive) setLiveQuote(d.quotes?.[0] ?? null); })
        .catch(() => {});
    }, 400);
    return () => { alive = false; clearTimeout(t); };
  }, [sym]);
  const pq = quotes[sym] ?? liveQuote;
  const preview = pq?.price ?? null;
  const previewEod = pq?.marketState === 'EOD';

  // The TradingView pane follows the ticker box (debounced so it doesn't
  // reload per keystroke); an empty box falls back to SPY.
  React.useEffect(() => {
    const t = setTimeout(() => {
      if (/^[A-Z0-9.^-]{1,12}$/.test(sym)) setChartSym(sym);
      else if (!sym) setChartSym('SPY');
    }, 600);
    return () => clearTimeout(t);
  }, [sym]);

  const place = async e => {
    e.preventDefault(); setBusy(true); setErr(null);
    try {
      await vgSend('/vantage/api/paper/order', 'POST', { symbol: sym, side: f.side, qty: Number(f.qty) });
      setF({ ...f, qty: '' });
      load();
    } catch (ex) { setErr(ex.message); }
    setBusy(false);
  };

  const book = state?.book;
  const t = book?.totals;
  const totalPnl = t ? (t.realized ?? 0) + (t.unrealized ?? 0) : null;

  return (
    <section style={{ background: VG.tile, borderRadius: 18, padding: '15px 18px', marginBottom: 14 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap', marginBottom: open ? 10 : 0 }}>
        <span style={vgS.caps}>paper desk · ghost trades, real prices</span>
        {t && (
          <span style={{ ...vgS.num, fontSize: 12.5, color: totalPnl == null ? VG.ink3 : vgDelta(totalPnl) }}>
            {vgMoney(totalPnl)} {t.unrealized == null && t.value == null ? '(realized only — no marks right now)' : ''}
          </span>
        )}
        {state?.pnlSeries?.length > 1 && (
          <span style={{ width: 110 }} title="paper P&L, one point per day">
            <VgLine series={[state.pnlSeries.map(r => r.v)]} colors={[VG.accent]} w={110} h={20} />
          </span>
        )}
        <span style={{ flex: 1 }} />
        <button onClick={() => setOpen(o => !o)} className="vg-chip" style={{
          appearance: 'none', border: 0, cursor: 'pointer', background: VG.chip,
          color: VG.ink3, borderRadius: 999, padding: '3px 12px', fontSize: 11,
        }}>{open ? 'collapse ▲' : 'expand ▾'}</button>
      </div>
      {open && (
        <React.Fragment>
          {/* The chart IS TradingView — no reinvented wheel. It follows the
              ticker input below; buy/sell capture Vantage's live quote at
              click time (same feed the whole app prices from), so what the
              order fills at is honest even when the chart's feed is delayed. */}
          <div style={{ marginBottom: 10 }}>
            <iframe key={chartSym} title="paper desk chart"
              src={`https://s.tradingview.com/widgetembed/?symbol=${encodeURIComponent(chartSym)}&interval=D&theme=dark&style=1&locale=en&hide_side_toolbar=1&withdateranges=1&saveimage=0&hide_volume=0&allow_symbol_change=0`}
              style={{ width: '100%', height: 380, border: 0, borderRadius: 12, background: '#000' }} />
            <div style={{ fontSize: 10.5, color: VG.ink4, marginTop: 4 }}>
              chart by tradingview (delayed on free feeds) · the ticker box below drives the chart ·
              fills capture vantage's live quote at the moment you click, slippage modeled against you
            </div>
          </div>
          <form onSubmit={place} style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', marginBottom: 12 }}>
            <VgInput value={f.symbol} onChange={e => setF({ ...f, symbol: e.target.value })} placeholder="SPY"
              style={{ width: 92, textTransform: 'uppercase' }} />
            <div style={{ display: 'flex', gap: 3, background: VG.chip, borderRadius: 10, padding: 3 }}>
              {['buy', 'sell'].map(s => (
                <button key={s} type="button" onClick={() => setF({ ...f, side: s })} style={{
                  appearance: 'none', border: 0, cursor: 'pointer', padding: '6px 16px',
                  borderRadius: 8, fontSize: 12, fontWeight: 700,
                  background: f.side === s ? VG.tile2 : 'transparent',
                  color: f.side === s ? (s === 'buy' ? VG.up : VG.down) : VG.ink3,
                }}>{s}</button>
              ))}
            </div>
            <VgInput value={f.qty} onChange={e => setF({ ...f, qty: e.target.value })} placeholder="qty" inputMode="decimal" style={{ width: 80 }} />
            <VgBtn kind="primary" small type="submit" disabled={busy || !sym || !Number(f.qty)}>place order</VgBtn>
            {preview != null && (
              <span style={{ fontSize: 11.5, color: VG.ink3 }}>
                {sym} at <b style={{ ...vgS.num, color: VG.ink }}>{vgMoney(preview)}</b>{previewEod ? ' (eod close)' : ''} ·
                fills {f.side === 'buy' ? 'a hair above' : 'a hair below'} — slippage max($0.01, 1bp) is modeled against you
              </span>
            )}
          </form>
          {err && <div style={{ color: VG.down, fontSize: 12.5, marginBottom: 10 }}>{err}</div>}

          {state === null && <div style={{ color: VG.ink3, fontSize: 12.5 }}>opening the desk…</div>}
          {state === undefined && <div style={{ color: VG.ink3, fontSize: 12.5 }}>paper desk unreachable — it returns when the server does.</div>}
          {book && book.positions.length === 0 && (
            <div style={{ color: VG.ink3, fontSize: 12.5 }}>
              no ghost positions yet — place a paper order and Vantage tracks it against real prices, slippage and all.
            </div>
          )}
          {book?.warnings?.length > 0 && (
            <div style={{ color: '#fb923c', fontSize: 11.5, marginBottom: 8 }}>▲ {book.warnings.join(' · ')}</div>
          )}

          {book && book.positions.length > 0 && (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 12 }}>
              <div style={{ display: 'grid', gridTemplateColumns: 'minmax(60px,1fr) 70px 80px 80px 90px 90px', gap: 8, ...vgS.caps, fontSize: 8.5, padding: '0 2px 4px' }}>
                <span>symbol</span><span style={{ textAlign: 'right' }}>qty</span><span style={{ textAlign: 'right' }}>avg fill</span>
                <span style={{ textAlign: 'right' }}>mark</span><span style={{ textAlign: 'right' }}>open p&l</span><span style={{ textAlign: 'right' }}>realized</span>
              </div>
              {book.positions.map(p => (
                <div key={p.symbol} style={{ display: 'grid', gridTemplateColumns: 'minmax(60px,1fr) 70px 80px 80px 90px 90px', gap: 8, fontSize: 12.5, ...vgS.num, padding: '3px 2px', borderTop: `1px solid ${VG.rule}` }}>
                  <span style={{ fontWeight: 600, color: VG.ink }}>{p.symbol}</span>
                  <span style={{ textAlign: 'right' }}>{p.qty ? vgNum(p.qty, 4) : '—'}</span>
                  <span style={{ textAlign: 'right' }}>{p.qty ? vgMoney(p.avgCost) : '—'}</span>
                  <span style={{ textAlign: 'right', color: p.mark == null ? VG.ink4 : VG.ink2 }}>{p.mark != null ? vgMoney(p.mark) : 'no mark'}</span>
                  <span style={{ textAlign: 'right', color: vgDelta(p.unrealized) }}>{vgMoney(p.unrealized)}</span>
                  <span style={{ textAlign: 'right', color: vgDelta(p.realized) }}>{vgMoney(p.realized)}</span>
                </div>
              ))}
            </div>
          )}

          {state?.orders?.length > 0 && (
            <div>
              <div style={{ ...vgS.caps, marginBottom: 5 }}>recent fills</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
                {state.orders.slice(0, 8).map(o => (
                  <div key={o.id} style={{ display: 'flex', gap: 10, fontSize: 12, alignItems: 'baseline', flexWrap: 'wrap' }}>
                    <span style={{ ...vgS.caps, color: o.side === 'buy' ? VG.up : VG.down, width: 30 }}>{o.side}</span>
                    <span style={{ ...vgS.num, color: VG.ink }}>{vgNum(o.qty, 4)} {o.symbol} @ {vgMoney(o.fill_price)}</span>
                    <span style={{ ...vgS.num, color: VG.ink4 }}>ref {vgMoney(o.ref_price)} · slip {vgMoney(o.slippage)}</span>
                    {o.basis === 'eod' && <span style={{ ...vgS.caps, color: '#fb923c' }} title={o.eod_date ? `filled on the ${o.eod_date} close` : ''}>eod basis</span>}
                    <span style={{ ...vgS.num, fontSize: 10.5, color: VG.ink4 }}>{vgDate(o.at)}</span>
                  </div>
                ))}
              </div>
            </div>
          )}
        </React.Fragment>
      )}
    </section>
  );
}

function VgSectors({ rows }) {
  const total = rows.reduce((a, r) => a + (r.value ?? r.cost), 0);
  if (!total) return null;
  const by = {};
  for (const r of rows) {
    const s = r.sector || 'Unclassified';
    by[s] = (by[s] || 0) + (r.value ?? r.cost);
  }
  const entries = Object.entries(by).sort((a, b) => b[1] - a[1]);
  const top = entries[0];
  return (
    <div style={{ background: VG.tile, borderRadius: 18, padding: '16px 18px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10 }}>
        <span style={vgS.caps}>sector allocation</span>
        {top && top[1] / total > 0.5 && (
          <VgTag color={VG.accent2}>{Math.round(top[1] / total * 100)}% in {top[0].toLowerCase()} — concentrated</VgTag>
        )}
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
        {entries.map(([s, v]) => (
          <div key={s}>
            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 }}>
              <span style={{ color: VG.ink2 }}>{s}</span>
              <span style={{ ...vgS.num, color: VG.ink }}>{Math.round(v / total * 100)}%</span>
            </div>
            <VgBar pct={v / total} color={VG.accent} h={3} />
          </div>
        ))}
      </div>
    </div>
  );
}

function VgPositionForm({ initial, onDone, onClose }) {
  const [f, setF] = React.useState(() => initial || {
    symbol: '', shares: '', costBasis: '', purchaseDate: new Date().toISOString().slice(0, 10), sector: '',
  });
  const [err, setErr] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const set = k => e => setF({ ...f, [k]: e.target.value });
  const submit = async e => {
    e.preventDefault(); setBusy(true); setErr(null);
    try {
      const body = {
        symbol: f.symbol, shares: Number(f.shares), costBasis: Number(f.costBasis),
        purchaseDate: f.purchaseDate, sector: f.sector,
      };
      if (initial?.id) await vgSend(`/vantage/api/position/${initial.id}`, 'PUT', body);
      else await vgSend('/vantage/api/position', 'POST', body);
      onDone();
    } catch (ex) { setErr(ex.message); setBusy(false); }
  };
  return (
    <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <VgField label="symbol">
          <VgInput value={f.symbol} onChange={set('symbol')} placeholder="AAPL" autoFocus={!initial}
            disabled={!!initial} style={{ textTransform: 'uppercase' }} />
        </VgField>
        <VgField label="shares">
          <VgInput value={f.shares} onChange={set('shares')} placeholder="10" inputMode="decimal" />
        </VgField>
        <VgField label="avg cost / share">
          <VgInput value={f.costBasis} onChange={set('costBasis')} placeholder="187.50" inputMode="decimal" />
        </VgField>
        <VgField label="purchase date">
          <VgInput type="date" value={f.purchaseDate} onChange={set('purchaseDate')} />
        </VgField>
      </div>
      <VgField label="sector (GICS)">
        <VgInput list="vg-sectors" value={f.sector || ''} onChange={set('sector')} placeholder="Information Technology" />
        <datalist id="vg-sectors">{VG_SECTORS.map(s => <option key={s} value={s} />)}</datalist>
      </VgField>
      {err && <div style={{ color: VG.down, fontSize: 12.5 }}>{err}</div>}
      <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
        <VgBtn onClick={onClose}>cancel</VgBtn>
        <VgBtn kind="primary" type="submit" disabled={busy || !f.symbol || !f.shares || !f.costBasis}>
          {initial ? 'save changes' : 'add position'}
        </VgBtn>
      </div>
    </form>
  );
}

function VgPositionDetail({ row, onClose, onEdit, onRefresh, earnings, signals = {} }) {
  const [txns, setTxns] = React.useState(null);
  React.useEffect(() => {
    vgGet(`/vantage/api/transactions?symbol=${encodeURIComponent(row.symbol)}`)
      .then(d => setTxns(d.transactions)).catch(() => setTxns([]));
  }, [row.symbol]);
  const del = async () => {
    if (!confirm(`Close ${row.symbol}? The transaction history is kept.`)) return;
    await vgSend(`/vantage/api/position/${row.id}${row.price != null ? `?price=${row.price}` : ''}`, 'DELETE');
    onClose(); onRefresh();
  };
  const stat = (l, v, c) => (
    <div><div style={vgS.caps}>{l}</div><div style={{ ...vgS.num, fontSize: 15, color: c || VG.ink, marginTop: 2 }}>{v}</div></div>
  );
  return (
    <VgModal title={row.symbol + (row.name ? ` · ${row.name}` : '')} onClose={onClose} width={560}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 16 }}>
        {stat('shares', vgNum(row.shares, 4))}
        {stat('avg cost', vgMoney(row.cost_basis))}
        {stat('price', vgMoney(row.price))}
        {stat('value', vgMoney(row.value ?? row.cost))}
        {stat('p&l', `${vgMoney(row.pnl$)} · ${vgPct(row.pnlPct)}`, vgDelta(row.pnl$))}
        {stat('held since', vgDate(row.purchase_date))}
      </div>
      {earnings?.[row.symbol]?.date && (
        <div style={{ ...vgS.num, fontSize: 12, color: '#fb923c', marginBottom: 14 }}>
          ▲ next earnings {vgDate(earnings[row.symbol].date)} · a volatility event for this position
        </div>
      )}
      {(() => {
        const c = signals?.[row.symbol]?.character;
        if (!c || c.z == null || Math.abs(c.z) < 1.5) return null;
        return (
          <div style={{ fontSize: 12, color: VG.accent2, marginBottom: 10, lineHeight: 1.5 }}>
            ◆ price character: {Math.abs(c.z).toFixed(1)}σ {c.z > 0 ? 'above' : 'below'} its recent mean
            {c.percentile != null ? ` (${c.percentile}th percentile of the window)` : ''}{c.trend ? `, ${c.trend}` : ''} — out of character.
          </div>
        );
      })()}
      {signals?.[row.symbol]?.coverage?.thread && (
        <div style={{ fontSize: 12, color: '#fb923c', marginBottom: 10, lineHeight: 1.5 }}>
          ◆ an active “{signals[row.symbol].coverage.thread.theme}” thread is running through {signals[row.symbol].coverage.thread.sector}
          {' '}({signals[row.symbol].coverage.thread.count} stories) — check whether this name is moving with it.
        </div>
      )}
      <div style={{ ...vgS.caps, marginBottom: 8 }}>transaction history</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 5, marginBottom: 18 }}>
        {txns === null && <span style={{ color: VG.ink3, fontSize: 12.5 }}>loading…</span>}
        {txns?.length === 0 && <span style={{ color: VG.ink3, fontSize: 12.5 }}>no transactions recorded.</span>}
        {(txns || []).map(t => (
          <div key={t.id} style={{
            display: 'grid', gridTemplateColumns: '52px 1fr auto auto', gap: 12, alignItems: 'baseline',
            padding: '8px 12px', borderRadius: 10, background: VG.chip, fontSize: 12.5,
          }}>
            <span style={{ ...vgS.caps, color: t.kind === 'sell' ? VG.down : t.kind === 'buy' ? VG.up : VG.ink3 }}>{t.kind}</span>
            <span style={{ color: VG.ink2 }}>
              {t.shares_delta != null ? `${t.shares_delta > 0 ? '+' : ''}${vgNum(t.shares_delta, 4)} sh` : (t.note || '—')}
            </span>
            <span style={{ ...vgS.num, color: VG.ink }}>{t.price != null ? vgMoney(t.price) : ''}</span>
            <span style={{ ...vgS.num, color: VG.ink4 }}>{vgDate(t.at)}</span>
          </div>
        ))}
      </div>
      <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
        <VgBtn kind="danger" onClick={del}>close position</VgBtn>
        <VgBtn kind="primary" onClick={onEdit}>edit</VgBtn>
      </div>
    </VgModal>
  );
}

// "Reports in 6d" — an earnings date within the window is a risk event the
// row should wear, not a fact buried in a modal.
function vgEarningsChip(earnings, symbol, windowDays = 14) {
  const e = earnings?.[symbol];
  if (!e?.date) return null;
  const days = Math.ceil((e.date - Date.now()) / 86400000);
  if (days < 0 || days > windowDays) return null;
  return days === 0 ? 'reports today' : days === 1 ? 'reports tomorrow' : `reports in ${days}d`;
}

// "Out of character" — the holding's price action expressed as a z-score
// against its OWN recent mean, not a raw day%. Only surfaces past 2σ, where
// "unusual" is a number rather than a vibe. minZ is looser in the detail modal.
function vgCharChip(signals, symbol, minZ = 2) {
  const c = signals?.[symbol]?.character;
  if (!c || c.z == null || Math.abs(c.z) < minZ) return null;
  const z = Math.abs(c.z).toFixed(1);
  return {
    text: `${c.z > 0 ? '+' : '−'}${z}σ`,
    title: `out of character — ${z}σ ${c.z > 0 ? 'above' : 'below'} its recent mean${c.percentile != null ? ` (${c.percentile}th pctile)` : ''}`,
  };
}

// A live news thread running through the sector this holding sits in — the
// funnel pointing at a name you own.
function vgThreadChip(signals, symbol) {
  const th = signals?.[symbol]?.coverage?.thread;
  if (!th) return null;
  return { text: th.theme, title: `an active "${th.theme}" thread is running through ${th.sector} (${th.count} stories)` };
}

// The 8-K stream: material filings for held + watched names in the last three
// weeks, newest first, material items called out in red. Symbol → dossier. Feed
// is EDGAR (keyless); degrades to a labeled state, never a silent blank.
function VgFilings({ onResearchSym }) {
  const [data, setData] = React.useState(undefined);   // undefined loading · null hard error · {filings,names,resolved}
  React.useEffect(() => {
    let alive = true;
    vgGet('/vantage/api/filings').then(d => alive && setData(d)).catch(() => alive && setData(null));
    return () => { alive = false; };
  }, []);
  if (data === undefined) return null;                 // quiet while loading
  if (!data || !data.names) return null;               // no book / hard error — nothing to say
  const filings = data.filings || [];
  return (
    <section style={{ background: VG.tile, borderRadius: 18, padding: '15px 18px', marginBottom: 14 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 10, flexWrap: 'wrap' }}>
        <span style={vgS.caps}>filings · your names · last 3 weeks</span>
        {data.resolved && filings.length > 0 && (
          <span style={{ fontSize: 11, color: VG.ink4 }}>{filings.length} 8-K{filings.length > 1 ? 's' : ''} · material ones flagged</span>
        )}
      </div>
      {!data.resolved ? (
        <div style={{ color: VG.ink3, fontSize: 12.5 }}>filings feed unreachable — your names' 8-Ks return when EDGAR is.</div>
      ) : filings.length === 0 ? (
        <div style={{ color: VG.ink3, fontSize: 12.5 }}>no 8-Ks from your names in the last 3 weeks — quiet is information too.</div>
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          {filings.map((f, i) => (
            <div key={i} style={{
              display: 'grid', gridTemplateColumns: '58px 84px 1fr auto', gap: 12, alignItems: 'baseline',
              padding: '7px 2px', borderBottom: i < filings.length - 1 ? `1px solid ${VG.rule}` : 'none', fontSize: 12.5,
            }}>
              <button onClick={() => onResearchSym?.(f.symbol)} title={`open ${f.symbol} dossier`} style={{
                appearance: 'none', border: 0, background: 'transparent', cursor: 'pointer', textAlign: 'left',
                ...vgS.num, color: VG.ink, fontWeight: 600, padding: 0,
              }}>{f.symbol}</button>
              <span style={{ ...vgS.num, color: VG.ink4 }}>{f.date}</span>
              <span style={{ color: VG.ink2, minWidth: 0 }}>
                {f.form}{f.items ? ` · items ${f.items}` : ''}
                {f.material && <span style={{ marginLeft: 8, color: VG.down, fontWeight: 600 }}>{f.materialItems.map(m => m.meaning).join('; ')}</span>}
              </span>
              {f.url
                ? <a href={f.url} target="_blank" rel="noreferrer" style={{ color: VG.ink3, fontSize: 11.5 }}>document ↗</a>
                : <span />}
            </div>
          ))}
        </div>
      )}
    </section>
  );
}

function VantagePortfolio({ positions, quotes, onRefresh, earnings, signals = {}, signalsSummary = null, watchlist = [], onWatchChanged, onResearchSym }) {
  const [range, setRange] = React.useState('3mo');
  const [bench, setBench] = React.useState(null);
  const [alpha, setAlpha] = React.useState({});
  const [adding, setAdding] = React.useState(false);
  const [editing, setEditing] = React.useState(null);
  const [detail, setDetail] = React.useState(null);

  const rows = positions.map(p => vgPositionRow(p, quotes[p.symbol]));
  const total = rows.reduce((a, r) => a + (r.value ?? r.cost), 0);

  // The dep key must cover EVERY input to the server-side bench math —
  // cost basis and purchase date included, or an edit leaves a stale chart.
  React.useEffect(() => {
    let alive = true;
    if (!positions.length) { setBench(null); return; }
    vgGet(`/vantage/api/bench?range=${range}`)
      .then(d => { if (alive) setBench(d.bench ? { ...d.bench, range: d.range } : null); })
      .catch(() => { if (alive) setBench(null); });
    return () => { alive = false; };
  }, [positions.map(p => `${p.symbol}|${p.shares}|${p.cost_basis}|${p.purchase_date}`).join(','), range]);

  // Per-holding alpha vs the S&P since each purchase — refreshed when the
  // book changes; degrades to blank cells, never to guessed numbers.
  React.useEffect(() => {
    if (!positions.length) { setAlpha({}); return; }
    let alive = true;
    vgGet('/vantage/api/alpha').then(d => alive && setAlpha(d.rows || {})).catch(() => {});
    return () => { alive = false; };
  }, [positions.map(p => p.symbol + p.purchase_date).join(',')]);

  const detailRow = detail != null ? rows.find(r => r.id === detail) : null;

  return (
    <React.Fragment>
      <VgSummary rows={rows} bench={bench} />

      <section style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.7fr) minmax(0, 1fr)', gap: 10, marginBottom: 14 }}>
        <div style={{ background: VG.tile, borderRadius: 18, padding: '16px 18px', minWidth: 0 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
            <span style={vgS.caps}>growth of $ invested · you vs s&p 500</span>
            <VgRangeToggle options={['1mo', '3mo', '6mo', '1y']} value={range} onChange={setRange} />
          </div>
          {bench ? (
            <React.Fragment>
              <VgLine series={[bench.portIdx, bench.spxIdx]} colors={[VG.accent, VG.ink3]} w={560} h={130} />
              <div style={{ display: 'flex', gap: 16, marginTop: 8, fontSize: 11.5 }}>
                <span style={{ color: VG.accent }}>— you {vgPct(bench.port)}</span>
                <span style={{ color: VG.ink3 }}>┄ s&p 500 {vgPct(bench.spx)}</span>
              </div>
            </React.Fragment>
          ) : (
            <div style={{ color: VG.ink3, fontSize: 12.5, padding: '28px 0' }}>
              {positions.length ? 'market data unavailable — chart returns when quotes do.' : 'add a position to see growth vs the index.'}
            </div>
          )}
        </div>
        <VgSectors rows={rows} />
      </section>

      {signalsSummary?.line && (
        <div style={{
          background: VG.tile, borderRadius: 14, padding: '11px 16px', marginBottom: 14,
          borderLeft: `2px solid ${VG.accent}`, display: 'flex', gap: 12, alignItems: 'baseline', flexWrap: 'wrap',
        }}>
          <span style={vgS.caps}>book signals</span>
          <span style={{ fontSize: 12.5, color: VG.ink2, lineHeight: 1.5, flex: 1, minWidth: 220 }}>{signalsSummary.line}</span>
        </div>
      )}

      {/* PM-Track watchlist table (ticker · added · note) — also fed by /watch and /unwatch. */}
      <VgWatchlistTable watchlist={watchlist} quotes={quotes} onWatchChanged={onWatchChanged} onResearchSym={onResearchSym} />

      <section style={{ background: VG.tile, borderRadius: 18, padding: '14px 0 6px', marginBottom: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 18px 10px' }}>
          <span style={vgS.caps}>positions · {rows.length}</span>
          <VgBtn kind="primary" small onClick={() => setAdding(true)}>+ add position</VgBtn>
        </div>
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5, minWidth: 720 }}>
            <thead>
              <tr>
                {['symbol', 'shares', 'avg cost', 'price', 'day', 'p&l', 'p&l %', 'α vs s&p', 'weight'].map((h, i) => (
                  <th key={h} style={{
                    ...vgS.caps, textAlign: i === 0 ? 'left' : 'right', fontWeight: 500,
                    padding: '6px 12px', borderBottom: `1px solid ${VG.rule}`,
                    paddingLeft: i === 0 ? 18 : 12, paddingRight: i === 8 ? 18 : 12,
                  }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.length === 0 && (
                <tr><td colSpan={9} style={{ padding: '22px 18px', color: VG.ink3, fontSize: 12.5 }}>
                  no positions yet — add your first and Vantage starts measuring it against the market.
                </td></tr>
              )}
              {rows.map(r => {
                const weight = total > 0 ? (r.value ?? r.cost) / total : 0;
                const td = { padding: '9px 12px', textAlign: 'right', ...vgS.num, borderBottom: `1px solid ${VG.rule}` };
                const charChip = vgCharChip(signals, r.symbol);
                const threadChip = vgThreadChip(signals, r.symbol);
                return (
                  <tr key={r.id} className="vg-row" onClick={() => setDetail(r.id)}>
                    <td style={{ ...td, textAlign: 'left', paddingLeft: 18 }}>
                      <span style={{ fontWeight: 600, color: VG.ink }}>{r.symbol}</span>
                      {weight > 0.25 && <span style={{ ...vgS.caps, color: VG.accent2, marginLeft: 8 }}>◆ 25%+</span>}
                      {charChip && <span title={charChip.title} style={{ ...vgS.caps, color: VG.accent2, marginLeft: 8 }}>◆ {charChip.text}</span>}
                      {threadChip && <span title={threadChip.title} style={{ ...vgS.caps, color: '#fb923c', marginLeft: 8 }}>◆ {threadChip.text}</span>}
                      {vgEarningsChip(earnings, r.symbol) && (
                        <span style={{ ...vgS.caps, color: '#fb923c', marginLeft: 8 }}>▲ {vgEarningsChip(earnings, r.symbol)}</span>
                      )}
                      <div style={{ fontSize: 10.5, color: VG.ink4 }}>{r.sector || (r.price == null ? 'no quote — showing cost' : '')}</div>
                    </td>
                    <td style={td}>{vgNum(r.shares, 4)}</td>
                    <td style={td}>{vgMoney(r.cost_basis)}</td>
                    <td style={td}>{vgMoney(r.price)}</td>
                    <td style={{ ...td, color: vgDelta(r.dayPct) }}>{vgPct(r.dayPct)}</td>
                    <td style={{ ...td, color: vgDelta(r.pnl$) }}>{vgMoney(r.pnl$)}</td>
                    <td style={{ ...td, color: vgDelta(r.pnlPct) }}>{vgPct(r.pnlPct)}</td>
                    <td style={{ ...td, color: vgDelta(alpha[r.symbol]?.alpha) }}
                      title={alpha[r.symbol] ? `you ${vgPct(alpha[r.symbol].ret)} vs S&P ${vgPct(alpha[r.symbol].spx)} since ${alpha[r.symbol].from}` : 'needs price history reaching the purchase date'}>
                      {vgPct(alpha[r.symbol]?.alpha)}
                    </td>
                    <td style={{ ...td, paddingRight: 18, minWidth: 90 }}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'flex-end' }}>
                        <span>{(weight * 100).toFixed(1)}%</span>
                        <span style={{ width: 40 }}><VgBar pct={weight} h={3} /></span>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </section>

      <VgFilings onResearchSym={onResearchSym} />

      <VgFilingAlerts onResearchSym={onResearchSym} />

      <VgPaperDesk quotes={quotes} />

      <VgRisk positions={positions} earnings={earnings} />

      {adding && (
        <VgModal title="Add position" onClose={() => setAdding(false)}>
          <VgPositionForm onClose={() => setAdding(false)} onDone={() => { setAdding(false); onRefresh(); }} />
        </VgModal>
      )}
      {editing && (
        <VgModal title={`Edit ${editing.symbol}`} onClose={() => setEditing(null)}>
          <VgPositionForm
            initial={{ id: editing.id, symbol: editing.symbol, shares: String(editing.shares), costBasis: String(editing.cost_basis), purchaseDate: String(editing.purchase_date).slice(0, 10), sector: editing.sector || '' }}
            onClose={() => setEditing(null)}
            onDone={() => { setEditing(null); onRefresh(); }} />
        </VgModal>
      )}
      {detailRow && !editing && (
        <VgPositionDetail row={detailRow} onClose={() => setDetail(null)}
          onEdit={() => setEditing(detailRow)} onRefresh={onRefresh} earnings={earnings} signals={signals} />
      )}
    </React.Fragment>
  );
}

window.VantagePortfolio = VantagePortfolio;
