// AsymmetricTheses — surface the highest-asymmetry investment ideas // the team has scored, plus the graveyard of names that aged out // (preserved per the user's directive — never deleted, just rotated). // // Pulls from /api/theses/ranked which walks every memo with // asymmetry_score in its frontmatter (Growth Arbitrage Analyst is the // primary producer; any persona can write the field). Click a ticker // to drill into TickerContext for full team view. // // The point of this panel: the user trades on asymmetric information. // This is the at-a-glance "what's the highest-edge idea on the board // right now" surface — distilled from every memo the team has filed. const { useState: aUseState, useEffect: aUseEffect, useCallback: aUseCallback, useMemo: aUseMemo } = React; function AsymmetricTheses({ open, onClose }) { if (!open) return null; const [data, setData] = aUseState(null); const [loading, setLoading] = aUseState(true); const [err, setErr] = aUseState(''); const [view, setView] = aUseState('top'); // 'top' | 'graveyard' const load = aUseCallback(async () => { setLoading(true); setErr(''); try { const r = await fetch('/api/theses/ranked?limit_top=15&limit_graveyard=15'); if (!r.ok) throw new Error(`HTTP ${r.status}`); const j = await r.json(); setData(j); } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }, []); aUseEffect(() => { if (open) load(); }, [open, load]); const list = view === 'top' ? (data?.top || []) : (data?.graveyard || []); return (
e.stopPropagation()} style={{ width: '100%', maxWidth: 820, maxHeight: 'calc(100vh - 80px)', overflowY: 'auto', background: 'var(--bg)', border: '1px solid var(--line-2)', borderRadius: 6, padding: 20, fontFamily: 'var(--font-body)', }}> {/* Header */}

Asymmetric theses

EV/GP/RG · ranked by asymmetry {data?.median != null && ( · median {data.median} )}
{err && (
{err}
)} {/* View toggle */}
{[ { id: 'top', label: 'Top tier', count: data?.top?.length || 0, hint: 'Asymmetry score ≥ 70 — verified high-edge ideas' }, { id: 'graveyard', label: 'Graveyard', count: data?.graveyard?.length || 0, hint: 'Sustained weakness (2+ memos < 50). Preserved — info is never thrown away.' }, ].map(t => ( ))} n={data?.n_scored || 0} scored
{loading && !data && (
Loading rankings…
)} {!loading && list.length === 0 && (
{view === 'top' ? ( <> No top-tier theses yet. The Growth Arbitrage Analyst fires weekdays at 04:30 ET — first run will populate this list. Or fire it manually via Memos panel → filter to "growth-arbitrage-analyst" → Fire now. ) : ( <>No graveyard entries — every scored thesis is currently in good standing. )}
)} {/* Ranking list */} {list.map((row, i) => { const isTop = view === 'top'; const accent = isTop ? '#5fb37c' : '#d9a85f'; return (
#{i + 1} {row.score} {row.evgprg != null && ( EV/GP/RG {row.evgprg.toFixed(2)} )} {new Date(row.ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric', })} {row.history_count > 1 && ` · ${row.history_count} memos`}
{row.topic && (
{row.topic}
)}
{row.role} {!isTop && row.last_strong_ts && ( last strong: {new Date(row.last_strong_ts).toLocaleDateString()} )} · {row.path}
); })}
); } window.AsymmetricTheses = AsymmetricTheses;