// TickerContext — single-glance "what does the team think about $X" modal. // // Opens whenever a ticker chip is clicked anywhere in the app. Aggregates: // - Position info (if held — qty, mark, P&L, sizing) // - Last 5 persona memos mentioning the ticker (verdict + role chips) // - Last 5 twitter signals about it (matched keywords + handle) // - Inline "Fire " buttons to wake the team on this name // - Link to the deep dive note in 4-Stocks/Deep Dives/$XXX.md // // Designed for the info-bias trader: when a tweet pops about NVDA, this // is the one click that surfaces "what's the standing read, and is what // I'm seeing news or noise the team has already chewed on?". const { useState: tcUseState, useEffect: tcUseEffect, useCallback: tcUseCallback } = React; const TC_ROLE_META = { 'phd-researcher': { label: 'PhD', color: '#8db4d8' }, 'hedging-specialist': { label: 'Hedging', color: '#d96a6a' }, 'framework-portfolio-modeling': { label: 'Sizing', color: '#5fb37c' }, 'framework-fundamentals': { label: 'Fundamentals', color: '#d9a85f' }, 'framework-value-investing': { label: 'Value', color: '#b48dd8' }, 'framework-growth-investing': { label: 'Growth', color: '#7fc7d9' }, 'framework-mental-models': { label: 'Mental', color: '#d8a07f' }, 'managing-director': { label: 'MD', color: '#ffffff' }, 'jarvis': { label: 'Tibeb', color: 'var(--accent)' }, 'editor-jarvis': { label: 'Editor', color: 'var(--accent)' }, 'regime-watcher': { label: 'Regime', color: '#ff9c5e' }, 'promotion-proposer': { label: 'Promote', color: '#5fd9b3' }, }; // Personas worth firing-on-demand for a ticker question const TC_FIRE_TARGETS = [ { role: 'phd-researcher', label: 'PhD scan' }, { role: 'framework-portfolio-modeling', label: 'Sizing read' }, { role: 'hedging-specialist', label: 'Hedge check' }, { role: 'framework-fundamentals', label: 'Fundamentals' }, ]; function TickerContext({ ticker, onClose }) { if (!ticker) return null; const T = ticker.replace(/^\$/, '').toUpperCase(); const [memos, setMemos] = tcUseState([]); const [signals, setSignals] = tcUseState([]); const [position, setPosition] = tcUseState(null); const [loading, setLoading] = tcUseState(true); const [err, setErr] = tcUseState(''); const [firing, setFiring] = tcUseState({}); // role → bool const [fireMsg, setFireMsg] = tcUseState({}); // role → string const load = tcUseCallback(async () => { setLoading(true); setErr(''); try { const [memosR, sigR, pfR] = await Promise.all([ fetch(`/api/personas/memos?ticker=${encodeURIComponent(T)}&limit=8`).catch(() => null), fetch(`/api/twitter/signals?limit=50`).catch(() => null), fetch(`/api/portfolio`).catch(() => null), ]); if (memosR?.ok) { const j = await memosR.json(); setMemos(j.memos || []); } if (sigR?.ok) { const j = await sigR.json(); // Filter signals to those mentioning the ticker (case-insensitive // text scan; the server's tweet text is what the user wants) const tickerRe = new RegExp(`\\b\\$?${T}\\b`, 'i'); const filtered = (j.signals || []).filter(s => tickerRe.test(s.text || '')); setSignals(filtered.slice(0, 8)); } if (pfR?.ok) { const j = await pfR.json(); const pos = (j.equities || []).concat(j.options || []).find(p => (p.ticker || '').toUpperCase() === T ); setPosition(pos || null); } } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }, [T]); tcUseEffect(() => { load(); }, [load]); const fire = tcUseCallback(async (role) => { if (firing[role]) return; setFiring(f => ({ ...f, [role]: true })); setFireMsg(m => ({ ...m, [role]: 'firing…' })); try { const r = await fetch(`/api/personas/${encodeURIComponent(role)}/fire`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic: `Quick read on $${T}: what's the standing thesis, what's changed in the last week, and what's the one trigger I should watch for next?`, trigger: 'ticker-drilldown', user_data: { ticker: T }, }), }); if (!r.ok) { const t = await r.text(); throw new Error(`HTTP ${r.status}: ${t.slice(0, 80)}`); } setFireMsg(m => ({ ...m, [role]: 'queued · 30-60s' })); // Re-load after 35s + 70s to catch the new memo setTimeout(load, 35_000); setTimeout(load, 70_000); } catch (e) { setFireMsg(m => ({ ...m, [role]: `failed: ${e.message}` })); } finally { setFiring(f => ({ ...f, [role]: false })); setTimeout(() => setFireMsg(m => ({ ...m, [role]: '' })), 8000); } }, [firing, T, load]); return (
e.stopPropagation()} style={{ width: '100%', maxWidth: 800, maxHeight: 'calc(100vh - 80px)', overflowY: 'auto', background: 'var(--bg)', border: '1px solid var(--line-2)', borderRadius: 6, padding: 20, fontFamily: 'var(--font-body)', }}> {/* Header */}
${T} team context
{err && (
{err}
)} {/* Position summary */} {position && (
position · in book
qty {position.qty} cost ${position.avg_cost?.toFixed?.(2)} mark ${position.last_price?.toFixed?.(2)} = 0 ? '#5fb37c' : '#d96a6a', fontWeight: 700, }}> {(position.unrealized_pnl ?? 0) >= 0 ? '+' : ''} ${(position.unrealized_pnl ?? 0).toLocaleString()} {position.unrealized_pct != null && ( ({position.unrealized_pct >= 0 ? '+' : ''}{position.unrealized_pct.toFixed(1)}%) )} MV ${position.market_value?.toLocaleString?.()}
)} {!position && !loading && (
Not in the book. View as research target.
)} {/* Fire actions */}
ask the team
{TC_FIRE_TARGETS.map(t => { const meta = TC_ROLE_META[t.role]; const f = firing[t.role]; const msg = fireMsg[t.role]; return (
{msg && ( {msg} )}
); })}
{/* Memos — newest first */}
team's recent reads · {memos.length}
{memos.length === 0 && !loading && (
No memos mention ${T} yet. Fire one of the team above.
)} {memos.map(m => { const roleMeta = TC_ROLE_META[m.role] || { label: m.role, color: 'var(--text)' }; const ts = new Date(m.ts).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); return (
{roleMeta.label} {m.source || 'cron'} {m.verdict && ( verdict: {m.verdict} )} {ts}
{m.topic}
{m.excerpt && (
{m.excerpt}
)}
); })}
{/* Twitter signals */} {signals.length > 0 && (
twitter mentions · {signals.length}
{signals.map(s => { const ts = new Date(s.created || s.fired_at || s.posted_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); return (
@{s.handle} ♥ {s.engagement} {ts}
{(s.text || '').slice(0, 240)}{(s.text || '').length > 240 ? '…' : ''}
); })}
)}
); } window.TickerContext = TickerContext; // Global open helper — anything that wants to surface a ticker can call // window.openTickerContext("NVDA"). The app.jsx mounts the modal once // and listens for the event. Avoids prop-drilling through the tree. window.openTickerContext = function (ticker) { if (!ticker) return; window.dispatchEvent(new CustomEvent('tibeb:ticker', { detail: { ticker } })); };