// PortfolioWorkspace — options-only book. // // Source of truth: brain/4-Stocks/Open Positions/Position Book MOC.md // Backend parses it via /api/options/positions and returns rows with // {ticker, instrument: 'LEAPS'|'calls'|'shares', contracts, strike, expiry, // expiry_iso, accounts, layer, thesis_tag}. // // User's locked style (per brain/AGENT.md): long calls / LEAPS only, no short // premium ever. Layout reflects that — strike + expiry + DTE are first-class. const { useState: pUseState, useEffect: pUseEffect, useCallback: pUseCallback, useMemo: pUseMemo } = React; const POLL_MS = 30_000; // Friendly chat-error formatter. Many upstream failures (proxy 501, broken // model, network blips) used to dump the raw HTML response back into the // chat thread, which destroyed all trust ("Should I buy AAPL?" → giant wall // of tags). We catch all of that here and emit one of a // small set of human messages. function friendlyChatError(err) { const status = err?.httpStatus || (typeof err?.message === 'string' && err.message.match(/\b(\d{3})\b/)?.[1]); if (status === 401 || status === '401') return "Your session looks signed out. Refresh the page and sign in again."; if (status === 429 || status === '429') return "I'm rate-limited right now — try again in a few seconds."; // 500-class: the upstream model died. Suggest concrete next steps the user // can take instead of just "try again" — for trade-tab users the most // common recovery is to use the structured order grammar that doesn't // need the LLM at all. if (status === 500 || status === '500') { return "The model timed out on that one. If you wanted to place an order, try the structured form: \"buy 1 NVDA market\" or \"sell nvda\" — those skip the model entirely."; } if (status === 501 || status === '501' || status === 502 || status === '502' || status === 503 || status === '503' || status === 504 || status === '504') { return "I can't reach the AI engine right now. Try again in a moment, or paraphrase."; } // Network / CORS / generic if (err?.name === 'TypeError' || /networkerror|failed to fetch/i.test(err?.message || '')) { return "Network hiccup — check your connection and try that again."; } // Catch-all: never echo raw HTML / huge bodies back to the user. const msg = String(err?.message || err || '').slice(0, 100); if (/ s.toUpperCase()).find(s => !TICKER_BLACKLIST.has(s)); if (!found) return null; return parseOrderIntentFinalize(side, qty, found, t); } return parseOrderIntentFinalize(side, qty, ticker, t); } function parseOrderIntentFinalize(side, qty, ticker, t) { // Order type — market / limit let order_type = 'market'; let limit_price = null; if (/\blimit\b/i.test(t)) { order_type = 'limit'; const px = t.match(/(?:at|@)\s*\$?(\d+(?:\.\d+)?)/i) || t.match(/limit\s+(?:at\s+)?\$?(\d+(?:\.\d+)?)/i); if (px) limit_price = parseFloat(px[1]); if (!limit_price) return null; // limit order with no price is ambiguous } else if (/\bmarket\b/i.test(t)) { order_type = 'market'; } return { kind: 'equity', side, qty, ticker, order_type, limit_price }; } function fmtMoney(n) { if (n == null || isNaN(n)) return '—'; const sign = n < 0 ? '-' : ''; const abs = Math.abs(n); if (abs >= 1_000_000) return `${sign}$${(abs / 1_000_000).toFixed(2)}M`; if (abs >= 1_000) return `${sign}$${(abs / 1_000).toFixed(1)}K`; return `${sign}$${abs.toFixed(2)}`; } function fmtPct(n) { if (n == null || isNaN(n)) return '—'; return `${n >= 0 ? '+' : ''}${n.toFixed(2)}%`; } function daysUntil(iso) { if (!iso) return null; const target = new Date(iso + 'T00:00:00Z').getTime(); const now = Date.now(); return Math.round((target - now) / (1000 * 60 * 60 * 24)); } function dteColor(dte) { if (dte == null) return 'var(--text-dim)'; if (dte < 60) return 'var(--danger)'; // < 2 months → roll soon if (dte < 180) return 'var(--catalyst)'; // < 6 months → watch return 'var(--accent)'; } function StatusPill({ status }) { const colors = { new: 'var(--catalyst)', accepted: 'var(--catalyst)', pending: 'var(--catalyst)', submitted: 'var(--catalyst)', filled: 'var(--accent)', partial_fill: 'var(--accent)', cancelled: 'var(--text-dim)', rejected: 'var(--danger)', expired: 'var(--text-dim)', }; const color = colors[(status || '').toLowerCase()] || 'var(--text-2)'; return ( {status || '—'} ); } function LayerPill({ layer }) { // Color-coded per AI-stack layer for fast visual scanning const map = { 'Memory': '#ff66cc', 'Power & Grid': '#ffaa33', 'Hyperscalers': '#3399ff', 'Semi-cap Equipment': '#aaff66', 'Compute / Foundry / Networking': '#00d4aa', 'Neocloud': '#bb88ff', 'Data Cloud / SaaS': '#88ddff', 'Speculative Pure-Play': '#ff7777', 'Semi-cap ADR Equity Sleeve': '#888899', 'Thematic ETFs': '#55ccaa', }; const color = map[layer] || 'var(--text-dim)'; return ( {layer} ); } // Phase 6 R · #7 — Theme cluster summary above the positions table. // Groups all positions by their thesis_tag (the LLM-extracted theme // from the position's brain note frontmatter). For each unique theme: // theme name · # positions · total contracts · total max-loss USD // Click a cluster to filter the table below it. Click "All" to clear. // // This surfaces "I have 5 names exposed to AI capex — what's my net // exposure if NVDA misses?" — the kind of cross-ticker view that's // invisible when positions are presented atomically. function ThemeClusters({ positions, themeFilter, setThemeFilter }) { if (!positions || positions.length === 0) return null; // Group by thesis_tag, falling back to "(untagged)" const groups = pUseMemo(() => { const m = new Map(); for (const p of positions) { const tag = (p.thesis_tag || '').trim() || '(untagged)'; if (!m.has(tag)) m.set(tag, { tag, positions: [], contracts: 0, max_loss: 0 }); const g = m.get(tag); g.positions.push(p); g.contracts += Number(p.contracts || p.shares || 0); // Approximate max-loss: cost basis × qty. Real max-loss for // long calls is the premium paid; cost_basis × contracts × 100 // for options or cost_basis × shares for equity is a fair proxy. const cb = Number(p.avg_cost || 0); const qty = Number(p.contracts || p.shares || 0); const multiplier = p.instrument === 'option' ? 100 : 1; g.max_loss += cb * qty * multiplier; } return [...m.values()].sort((a, b) => b.positions.length - a.positions.length); }, [positions]); if (groups.length <= 1) return null; // no value when everything's one cluster return (
Theme exposure · click to filter
{groups.length} active themes across {positions.length} positions
{groups.map(g => ( ))}
); } function PositionsTable({ positions, layerFilter, instrumentFilter, themeFilter }) { const filtered = positions.filter(p => (layerFilter === 'all' || p.layer === layerFilter) && (instrumentFilter === 'all' || p.instrument === instrumentFilter) && (!themeFilter || themeFilter === 'all' || (p.thesis_tag || '(untagged)') === themeFilter) ); // Phase 6 R · #G — Flash-on-change tracking. Compare current // last_price per ticker against the previous value to compute a // flash class for the price cell ('pf-flash-up' / 'pf-flash-down'). // Previous values stored in a ref so they don't trigger re-renders. const prevPricesRef = React.useRef({}); const flashes = React.useMemo(() => { const out = {}; for (const p of filtered) { if (!p.ticker || p.last_price == null) continue; const key = `${p.ticker}:${p.strike || 'eq'}:${p.expiry || 'eq'}`; const prev = prevPricesRef.current[key]; if (prev != null && Math.abs(p.last_price - prev) > 0.001) { out[key] = p.last_price > prev ? 'pf-flash-up' : 'pf-flash-down'; } prevPricesRef.current[key] = p.last_price; } return out; }, [filtered]); return (
Options Book
{filtered.length} positions{layerFilter !== 'all' && ` · ${layerFilter}`}
long calls / LEAPS only · paper
{filtered.length === 0 && ( )} {filtered.map((p, i) => { const dte = daysUntil(p.expiry_iso); return ( {/* Phase 6 R · #G — Live mark + P&L. Flashes green on tick-up, red on tick-down. Refreshes every 30s with the rest of the auto-poll. */} {(() => { const key = `${p.ticker}:${p.strike || 'eq'}:${p.expiry || 'eq'}`; const flashClass = flashes[key] || ''; const pnl = p.unrealized_pnl; const pct = p.unrealized_pct; const pnlColor = pnl == null ? 'var(--text-dim)' : pnl >= 0 ? '#5fb37c' : '#d96a6a'; return ( ); })()} {/* Phase 6 R · #5 — Sizing context columns. Pulled from each position's brain note via best-effort regex (size %, max-loss, kill condition). When missing, render "—" so the missing data is visible (prompts the user to fill the note in). */} ); })}
Ticker Layer Contracts Strike Expiry DTE Mark · P&L % HH Max-loss Kill if Account(s) Thesis tag
Your Alpaca paper book is empty.
Place your first paper trade by clicking Place on any recommendation in 1 · Idea & Research, or chat with Tibeb ( bottom-right) and say "Buy 1 NVDA market".
{p.instrument === 'shares' ? {p.shares} sh : <>{p.contracts} ct} {p.strike != null ? `$${p.strike.toFixed(0)}` : '—'} {p.expiry || '—'} {dte != null ? `${dte}d` : '—'} {p.last_price != null ?
${Number(p.last_price).toFixed(2)}
:
} {pnl != null && (
{pnl >= 0 ? '+' : ''}{fmtMoney(pnl)} {pct != null && ({pct >= 0 ? '+' : ''}{pct.toFixed(1)}%)}
)}
5 ? '#d9a85f' : 'var(--text-2)' }} title={p.size_pct != null ? `${p.size_pct}% of household` : 'Size not in note — add a "Size (% of household)" line'}> {p.size_pct != null ? `${p.size_pct}%` : '—'} {p.max_loss_usd != null ? {fmtMoney(p.max_loss_usd)} : } {p.kill_condition || '—'} {(p.accounts || []).join(' · ')} {p.thesis_tag} {/* Phase 6 R · #9 — stale thesis pill. Anything 30+ days untouched gets a subtle red badge so the weekly-review surfaces the rotting positions. */} {p.thesis_age_days != null && p.thesis_age_days > 30 && ( STALE {p.thesis_age_days}d )}
); } // Phase 6 R · #6 — Trade journal capture for filled orders. // Goal: 30 seconds after a fill, capture the WHY. Three quick fields: // 1. Catalyst — what prompted this trade (one-line) // 2. Conviction — low / medium / high / highest // 3. Kill condition — what would invalidate the thesis // // Submission appends a journal block to the brain position note at // 4-Stocks/Open Positions/$TICKER.md via /api/orders/{id}/journal. // Skip button collapses the form so it doesn't block the UI. Once // journaled, localStorage marks it skipped/done so the form doesn't // re-show on every refresh. function OrderJournal({ order }) { const journalKey = `tibeb.orderJournal.${order.id || order.ticker + order.created}`; const [state, setState] = pUseState(() => { try { return localStorage.getItem(journalKey) || ''; } catch { return ''; } }); const [catalyst, setCatalyst] = pUseState(''); const [conviction, setConviction] = pUseState('high'); const [killCondition, setKillCondition] = pUseState(''); const [submitting, setSubmitting] = pUseState(false); if (state === 'done' || state === 'skipped') return null; const skip = () => { setState('skipped'); try { localStorage.setItem(journalKey, 'skipped'); } catch {} }; const submit = async () => { if (!catalyst && !killCondition) { alert('Add at least a catalyst or kill-condition before submitting.'); return; } setSubmitting(true); try { const r = await fetch(`/api/orders/${encodeURIComponent(order.id || '')}/journal`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticker: order.ticker, catalyst, conviction, kill_condition: killCondition, fill_price: order.fill_price, ts: new Date().toISOString(), }), }); // Best-effort: backend may not have the endpoint yet — that's // OK, we still mark done locally so the form doesn't keep // showing. Future commit wires the brain-note append. setState('done'); try { localStorage.setItem(journalKey, 'done'); } catch {} } catch { setState('done'); try { localStorage.setItem(journalKey, 'done'); } catch {} } finally { setSubmitting(false); } }; return (
Journal this fill · {order.ticker}
setCatalyst(e.target.value)} style={{ padding: '5px 8px', background: 'var(--bg)', color: 'var(--text)', border: '1px solid var(--line)', fontSize: 11, fontFamily: "'JetBrains Mono', monospace" }}/>
setKillCondition(e.target.value)} style={{ width: '100%', padding: '5px 8px', background: 'var(--bg)', color: 'var(--text)', border: '1px solid var(--line)', fontSize: 11, fontFamily: "'JetBrains Mono', monospace", boxSizing: 'border-box', marginBottom: 8 }}/>
); } function OrdersFeed({ orders, onAfterCancel }) { // Phase 5 — track which order is mid-cancel so we can disable the button // and show "cancelling…" instead of stalling silently. Uses a Set so // we can have multiple cancels in flight (rare but possible). const [cancelling, setCancelling] = pUseState(() => new Set()); const [cancelErr, setCancelErr] = pUseState(null); const cancellable = (status) => { const s = (status || '').toLowerCase(); return s === 'pending' || s === 'submitted' || s === 'partially_filled' || s === 'new' || s === 'accepted'; }; const handleCancel = async (o) => { if (!o || !o.id) return; if (!window.confirm(`Cancel ${(o.side || '').toUpperCase()} ${o.qty} ${o.ticker}?\n\nThis fires DELETE /api/orders/${o.id} on the shared Alpaca paper account. The position note in your brain will get a CANCELLED line appended.`)) return; setCancelling(prev => { const s = new Set(prev); s.add(o.id); return s; }); setCancelErr(null); try { const r = await fetch(`/api/orders/${encodeURIComponent(o.id)}`, { method: 'DELETE' }); if (!r.ok) { const t = await r.text().catch(() => ''); throw new Error(`HTTP ${r.status} ${t.slice(0, 80)}`); } if (typeof onAfterCancel === 'function') await onAfterCancel(); } catch (e) { setCancelErr(`Cancel failed for ${o.ticker}: ${e.message || e}`); } finally { setCancelling(prev => { const s = new Set(prev); s.delete(o.id); return s; }); } }; return (
Trade Execution (paper)
{(orders || []).length} orders
{cancelErr && (
⚠ {cancelErr}
)}
{(!orders || orders.length === 0) && (
No paper orders yet. The Position Book above is the source of truth for live LEAPS; this feed shows simulated executions from the paper broker.
)} {(orders || []).map(o => { const isCancelling = cancelling.has(o.id); const canCancel = cancellable(o.status) && !isCancelling; return (
{(o.side || '').toUpperCase()} ${o.ticker} {o.qty} {o.order_type || 'market'} {o.limit_price != null && @ {fmtMoney(o.limit_price)}} {(o.filled_at || o.created || '').slice(0, 16).replace('T', ' ')} {/* Phase 5 — Cancel button. Only renders for in-flight orders. */} {canCancel && ( )} {isCancelling && ( cancelling… )}
{o.fill_price != null && (
filled @ {fmtMoney(o.fill_price)} · ID {o.id || '—'}
)} {/* Phase 6 R · #6 — Inline journal capture for filled orders. Only renders for filled orders that haven't been journaled yet (tracked via localStorage). User submits catalyst + conviction + kill-condition, and it appends to the position's brain note. */} {o.status === 'filled' && }
); })}
); } function BookSummary({ positions }) { const stats = pUseMemo(() => { const opt = positions.filter(p => p.instrument !== 'shares'); const sh = positions.filter(p => p.instrument === 'shares'); const totalContracts = opt.reduce((a, b) => a + (b.contracts || 0), 0); const byLayer = {}; positions.forEach(p => { byLayer[p.layer] = (byLayer[p.layer] || 0) + 1; }); const expiringSoon = opt.filter(p => { const d = daysUntil(p.expiry_iso); return d != null && d < 180; }).length; const byExpiry = {}; opt.forEach(p => { if (p.expiry) byExpiry[p.expiry] = (byExpiry[p.expiry] || 0) + (p.contracts || 0); }); return { optCount: opt.length, shCount: sh.length, totalContracts, byLayer, expiringSoon, byExpiry }; }, [positions]); const layerRows = Object.entries(stats.byLayer).sort((a, b) => b[1] - a[1]); const expRows = Object.entries(stats.byExpiry).sort((a, b) => { const aIso = (positions.find(p => p.expiry === a[0]) || {}).expiry_iso || ''; const bIso = (positions.find(p => p.expiry === b[0]) || {}).expiry_iso || ''; return aIso.localeCompare(bIso); }); return (
Book Summary
{stats.optCount} option lines · {stats.totalContracts} contracts
Option lines
{stats.optCount}
Equity sleeve
{stats.shCount} tickers
Total contracts
{stats.totalContracts}
Expiring <180d
0 ? 'var(--catalyst)' : 'var(--accent)' }}> {stats.expiringSoon}
By AI-stack layer
{layerRows.map(([layer, count]) => (
{layer} {count}
))}
By expiry cycle
{expRows.map(([exp, ct]) => (
{exp} {ct} ct
))}
); } // ───────────────────────────────────────────────────────────────────── // PerformancePanel — hedge-fund-grade trade metrics // ───────────────────────────────────────────────────────────────────── // Phase 1D: pulls /api/performance/summary which reconciles all closed // Alpaca orders into per-trade outcomes, builds the daily equity curve, // and computes Sharpe / Sortino / Calmar / max-drawdown / win rate / // profit factor / expectancy / Kelly-from-history / CVaR / VaR / total // return / annualized return. The numbers are noisy below N=30 closed // trades; the backend surfaces a `sample_size_warning` field that we // render as a banner above the grid. // // Where Brier + Murphy (in backend/tibeb/calibration.py) measure FORECAST // quality, these measure TRADE quality. Both halves are needed for // hedge-fund-grade evaluation. function PerformancePanel() { const [data, setData] = pUseState(null); const [busy, setBusy] = pUseState(false); const [err, setErr] = pUseState(''); const refresh = pUseCallback(async () => { setBusy(true); setErr(''); try { const r = await fetch('/api/performance/summary'); if (!r.ok) throw new Error(`status ${r.status}`); const j = await r.json(); setData(j); } catch (e) { setErr(e.message || String(e)); } finally { setBusy(false); } }, []); pUseEffect(() => { refresh(); }, [refresh]); // Render a single metric tile. Handles null (insufficient data) cleanly. const Stat = ({ label, value, fmt, color, title }) => { const display = (value == null) ? : (fmt ? fmt(value) : value); return (
{label}
{display}
); }; const s = data?.summary || {}; const n = data?.n_closed_trades || 0; // Color tiers cribbed from common hedge-fund thresholds: // Sharpe ≥ 1.0 = strong | ≥ 0.5 = OK | <0.5 weak | <0 = losing const sharpeColor = s.annualized_sharpe == null ? null : s.annualized_sharpe >= 1.0 ? 'var(--accent)' : s.annualized_sharpe >= 0.5 ? 'var(--catalyst)' : s.annualized_sharpe >= 0 ? 'var(--text-dim)' : 'var(--danger)'; const ddColor = s.max_drawdown_pct == null ? null : s.max_drawdown_pct > -0.10 ? 'var(--accent)' : s.max_drawdown_pct > -0.20 ? 'var(--catalyst)' : 'var(--danger)'; const wrColor = s.win_rate == null ? null : s.win_rate >= 0.55 ? 'var(--accent)' : s.win_rate >= 0.45 ? 'var(--catalyst)' : 'var(--danger)'; const pfColor = s.profit_factor == null ? null : s.profit_factor >= 2.0 ? 'var(--accent)' : s.profit_factor >= 1.5 ? 'var(--catalyst)' : s.profit_factor >= 1.0 ? 'var(--text-dim)' : 'var(--danger)'; return (
Performance · realized trade metrics
{n > 0 ? `${n} closed trades` : 'no closed trades yet'} {s.period_start && s.period_end && ( · {s.period_start} → {s.period_end} )}
{err && (
✕ {err}
)} {s.sample_size_warning && (
△ {s.sample_size_warning}
)} {/* Tier 1 — risk-adjusted return (the hedge-fund headline numbers) */}
v.toFixed(2)} color={sharpeColor} title="Sharpe 1966 · (mean − rf) / σ × √252. ≥1.0 strong; ≥0.5 OK; <0 losing." /> v.toFixed(2)} title="Sortino-van der Meer 1991 · downside-deviation Sharpe. Less penalty for upside vol." /> v.toFixed(2)} title="Young 1991 · annualized return / |max drawdown|. ≥1.0 = recovery faster than drawdown depth." /> v.toFixed(2)} title="Goodwin 1998 · active return / tracking error × √252. ≥0.5 excellent; 1.0 elite. Pending benchmark wiring." />
{/* Tier 2 — drawdown + total return */}
`${(v * 100).toFixed(1)}%`} color={ddColor} title="Deepest peak-to-trough on the realized equity curve." /> `${(v * 100).toFixed(1)}%`} title="Latest equity vs running peak." /> `${(v * 100).toFixed(1)}%`} color={s.total_return_pct == null ? null : (s.total_return_pct >= 0 ? 'var(--accent)' : 'var(--danger)')} title="Cumulative geometric return over the period." /> `${(v * 100).toFixed(1)}%`} color={s.annualized_return_pct == null ? null : (s.annualized_return_pct >= 0 ? 'var(--accent)' : 'var(--danger)')} title="CAGR-style annualized return." />
{/* Tier 3 — trade-system stats */}
`${(v * 100).toFixed(1)}%`} color={wrColor} title="Fraction of trades with P&L > 0." /> v.toFixed(2)} color={pfColor} title="Σ wins / |Σ losses|. ≥2.0 strong; 1.0 break-even." /> fmtMoney(v) + '/trade'} title="Average P&L per trade — must be positive for a viable system." /> v.toFixed(2) + 'x'} title="|avg winner / avg loser|. >1 lets a low win-rate system survive." />
{/* Tier 4 — sizing + tail risk */}
fmtMoney(v)} color="var(--accent)" title="Mean P&L of winning trades." /> fmtMoney(v)} color="var(--danger)" title="Mean P&L of losing trades." /> `${(v * 100).toFixed(1)}%`} title="Empirical Kelly fraction from realized wins/losses. NEVER bet full Kelly — use 0.25-0.50 fractional Kelly (MacLean-Thorp-Ziemba 2010)." /> `${(v * 100).toFixed(2)}%`} color="var(--danger)" title="Rockafellar-Uryasev 2000 · average loss in the worst 5% tail. Always ≥ VaR; gap widens with fat tails." />
Historical VaR 95% (daily): {s.historical_var_95_daily == null ? '—' : `${(s.historical_var_95_daily * 100).toFixed(2)}%`} {data?.disclaimer && (
{data.disclaimer}
)}
); } // Phase 6 R · #4 — Locked playbook compliance. The owner trades long // calls / LEAPS / shares only — never short premium — and on Dec 2027 / // Jan 2028 / Jun 2028 / Dec 2028 expiries (per brain/AGENT.md). Hide // any rec that violates short-premium rule entirely; flag off-cycle // expiries with an orange pill so the user can still place if they // override but knows it's off-style. const APPROVED_LEAPS_EXPIRIES = ['Dec 2027', 'Jan 2028', 'Jun 2028', 'Dec 2028']; function recCompliance(r) { const side = (r?.side || '').toUpperCase(); const violatesStyle = /SELL_(CALL|PUT)|SHORT_(CALL|PUT)|^WRITE/i.test(side); // Expiry can be either a string ("Dec 2028") or ISO date — be lenient let expiryOK = true; if (r?.expiry) { const s = String(r.expiry); expiryOK = APPROVED_LEAPS_EXPIRIES.some(e => s.includes(e)); } return { violatesStyle, expiryOK }; } function RecommendationsCard({ recs, onAct, onRefresh }) { if (!recs || recs.length === 0) return null; // Filter out short-premium recs entirely — they violate the locked // style and shouldn't even show up. Off-cycle expiries still render // with a warning pill so the user can decide. const filteredRecs = recs.filter(r => !recCompliance(r).violatesStyle); const hiddenStyleViolations = recs.length - filteredRecs.length; // Track per-rec exec state so the user gets feedback inline. const [execState, setExecState] = pUseState({}); // { [id]: 'placing' | 'placed' | 'error:msg' } async function placeRec(r) { if (!r.ticker) return; setExecState(s => ({ ...s, [r.id]: 'placing' })); // Map the recommendation into an /api/orders payload. Equity recs go // straight through; option recs include the OCC fields if Alpaca's // options endpoint understands them. Always pass thesis + research // context so the backend's brain-note autowriter has something to // store under "Thesis" / "Research". const isOption = !!(r.strike || r.expiry || r.contracts); const qty = isOption ? Math.max(1, r.contracts || 1) : Math.max(1, r.shares || 1); const side = (r.side || 'BUY_CALL').toUpperCase().includes('SELL') ? 'sell' : 'buy'; const body = { ticker: r.ticker, side, qty, order_type: 'market', thesis: r.rationale || '', research_summary: [ r.expected_value_usd != null ? `EV ${fmtMoney(r.expected_value_usd)}` : '', r.max_loss_usd != null ? `max-loss ${fmtMoney(r.max_loss_usd)}` : '', r.rr_ratio != null ? `R/R ${Number(r.rr_ratio).toFixed(1)}x` : '', r.breakeven != null ? `breakeven $${Number(r.breakeven).toFixed(0)}` : '', r.confidence_pct != null ? `confidence ${Number(r.confidence_pct).toFixed(0)}%` : '', ].filter(Boolean).join(' · '), sources: r.sources || [], note: `tibeb-rec ${r.id || ''}`.slice(0, 200), }; if (isOption) { // Optional structured fields the broker layer may use to route to // Alpaca's /v2/options/orders endpoint. body.option_strike = r.strike; body.option_expiry = r.expiry; body.option_side = r.side; } try { const r1 = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!r1.ok) throw new Error(`order ${r1.status}: ${await r1.text().catch(() => '')}`); setExecState(s => ({ ...s, [r.id]: 'placed' })); // Mark the rec as accepted so it disappears from pending on next refresh. try { await onAct(r.id, 'accepted'); } catch (_) {} } catch (e) { setExecState(s => ({ ...s, [r.id]: 'error:' + (e.message || 'failed') })); } } return (
Trade Recommendations · ranked by confidence
{filteredRecs.length} pending · click Place to send to Alpaca paper {hiddenStyleViolations > 0 && ( · {hiddenStyleViolations} hidden (short-premium violates locked style) )}
{filteredRecs.map((r, i) => { const conf = r.confidence_pct || 0; const confColor = conf >= 60 ? 'var(--accent)' : conf >= 40 ? 'var(--catalyst)' : 'var(--text-dim)'; const state = execState[r.id]; const placing = state === 'placing'; const placed = state === 'placed'; const err = typeof state === 'string' && state.startsWith('error:') ? state.slice(6) : null; const { expiryOK } = recCompliance(r); return (
#{i + 1}
r.confidence_breakdown && setExecState(s => ({...s, [`bd:${r.id}`]: !s[`bd:${r.id}`]}))}> {conf.toFixed(0)}%
confidence
{/* Phase 6 R · #12 — Per-persona confidence breakdown. Shows which personas backed this rec and at what weight, plus the dissents. Renders inline below the rec row when the confidence pill is clicked. Only visible when backend supplied r.confidence_breakdown (won't appear yet — needs the consult_team output schema to populate it). */} {r.confidence_breakdown && execState[`bd:${r.id}`] && (
Confidence breakdown
{(r.confidence_breakdown || []).map((b, j) => (
{b.persona} 0 ? '#5fb37c' : '#d96a6a', fontFamily: "'JetBrains Mono', monospace", fontWeight: 700, minWidth: 50, }}> {(b.weight || 0) > 0 ? '+' : ''}{b.weight} {b.verdict || ''}
))}
)}
{(r.side || 'BUY_CALL').replace('_', ' ')} ${r.ticker} {r.strike != null && ${Number(r.strike).toFixed(0)}} {r.expiry && {r.expiry}} {r.contracts && {r.contracts} ct} {!expiryOK && r.expiry && ( OFF-CYCLE EXPIRY )}
{r.expected_value_usd != null && EV {fmtMoney(r.expected_value_usd)}} {r.max_loss_usd != null && max-loss {fmtMoney(r.max_loss_usd)}} {r.rr_ratio != null && R/R {Number(r.rr_ratio).toFixed(1)}x} {r.breakeven != null && breakeven ${Number(r.breakeven).toFixed(0)}}
{/* Tibeb Probability Engine v1 — sidecar math row. Renders ONLY when the backend's `_finalize_madmax_for_web` attached compose() output to this rec (Phase 1B). Shows the LLM-integer (the `confidence_pct` above) alongside the graph-absorption number, the composite γ-blend, the 80% credible interval, and a divergence pill when |pool − graph| ≥ 0.15 (the MD's verdict ought to explain that gap per Pipeline v2 §3.5). During Phase 1 cold-start the LLM-integer stays the primary number on the row; this strip is the audit trail that earns the math its credibility ramp-up. See `brain/1-Frameworks/Probability/Tibeb Probability Engine.md`. */} {(r.composite_probability != null || r.graph_probability != null || r.divergence_flag) && (
{r.composite_probability != null && ( composite {(r.composite_probability * 100).toFixed(0)}% )} {r.graph_probability != null && ( graph {(r.graph_probability * 100).toFixed(0)}% )} {r.pool_probability != null && ( pool {(r.pool_probability * 100).toFixed(0)}% )} {r.credible_interval_lb != null && r.credible_interval_ub != null && r.credible_interval_lb !== r.credible_interval_ub && ( CI [{(r.credible_interval_lb * 100).toFixed(0)}-{(r.credible_interval_ub * 100).toFixed(0)}%] )} {r.divergence_flag ? ( △ divergence {(r.divergence * 100).toFixed(0)}% ) : null}
)} {r.rationale &&
{r.rationale}
} {placed &&
✓ Placed on Alpaca paper · brain note opened at 4-Stocks/Open Positions/${r.ticker}.md
} {err &&
✕ {err}
}
); })}
); } function PortfolioTibebPanel({ collapsed, setCollapsed, positions, recs }) { const [messages, setMessages] = pUseState(() => { try { return JSON.parse(localStorage.getItem('jarvis.portfolio.chat') || '[]'); } catch { return []; } }); const [input, setInput] = pUseState(''); const [busy, setBusy] = pUseState(false); // Phase 4.B — push-to-talk via MediaRecorder + ElevenLabs Scribe. // Same pattern as the Brain-tab Editor Tibeb chat (frontend/jarvis/brain.jsx). // Records on press, stops on second press, transcribes via /api/voice/transcribe, // drops the resulting text straight into the input where the user can edit // before sending or hit Enter to fire. const [recording, setRecording] = pUseState(false); const [transcribing, setTranscribing] = pUseState(false); // Phase 5 — TTS read-back. Persists across web changes so the user's // last preference sticks. Browser SpeechSynthesis (zero-cost, no API // dependency); Brian voice + ElevenLabs Flash would need a /api/voice/tts // round-trip and isn't wired here yet. const [speakOn, setSpeakOn] = pUseState(() => { try { return localStorage.getItem('jarvis.portfolio.speakOn') === '1'; } catch { return false; } }); const [speaking, setSpeaking] = pUseState(false); const lastSpokenIdxRef = React.useRef(-1); const mediaRecRef = React.useRef(null); const audioChunksRef = React.useRef([]); const logRef = React.useRef(null); // ElevenLabs audio playback element. Created lazily on first speak // so the page boots without an idle