// Pantalla de detalle de acción — chart interactivo + sidebar + tabs function StockDetail({ symbol, onBack, onOpenTicker, holdings, watchlist, isWatching, onToggleWatch, accent, intensity }) { const t = window.TICKERS[symbol]; const [tab, setTab] = React.useState('resumen'); if (!t) return null; const up = t.change >= 0; const holding = holdings.find((h) => h.symbol === symbol); const news = (window.NEWS[symbol] || window.NEWS.NVRA).slice(0, 6); const tabs = [ { id: 'resumen', label: 'Resumen' }, { id: 'noticias', label: 'Noticias' }, { id: 'fund', label: 'Fundamentales' }, { id: 'tecnico', label: 'Análisis técnico' }, { id: 'perfil', label: 'Perfil de la empresa' }, ]; const fmt = (n) => Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return (
{/* ── HEADER ── */}
{ e.preventDefault(); onBack(); }}>Inicio  /  {t.sector}  /  {t.name}
{t.symbol.slice(0, 2)}

{t.name}

{t.symbol}
{t.market} · {t.sector} · {t.cap}
${fmt(t.price)}
{up ? '+' : ''}${fmt(Math.abs(t.change))} {up ? '▲' : '▼'} {up ? '+' : ''}{t.pct.toFixed(2)}%
Hoy · 16:00 EDT · Mercado cerrado
{/* ── TABS ── */}
{tabs.map((tb) => ( ))}
{/* ─── PANEL IZQUIERDO ─── */}
{/* Quick stats */}
Apertura
${fmt(t.open)}
Máx. diario
${fmt(t.high)}
Mín. diario
${fmt(t.low)}
Volumen
{t.volume}
Mkt Cap
{t.mktcap}
{/* Chart */} {/* Contenido por tab */} {tab === 'resumen' && } {tab === 'noticias' && } {tab === 'fund' && } {tab === 'tecnico' && } {tab === 'perfil' && }
{/* ─── SIDEBAR DERECHO ─── */}
{/* Calculadora de análisis */} {/* Mi posición */} {holding && ( <>
Mi posición
Acciones {holding.shares}
Coste medio ${fmt(holding.avgCost)}
Valor de mercado ${fmt(t.price * holding.shares)}
P&L = holding.avgCost ? 'up' : 'dn'}`}> {t.price >= holding.avgCost ? '+' : ''}${fmt((t.price - holding.avgCost) * holding.shares)} {' · '} {(((t.price - holding.avgCost) / holding.avgCost) * 100).toFixed(2)}%
)}
Métricas clave
Capitalización{t.mktcap}
Volumen hoy{t.volume}
P/E Ratio{t.pe}
EPS (TTM){t.eps}
Dividendo{t.div}
Margen neto{t.margin}
Beta (5 años){t.beta}
Rango 52 semanas
Mín. ${fmt(t.range52.min)} ${fmt(t.price)} Máx. ${fmt(t.range52.max)}
Distancia al máx. −{(((t.range52.max - t.price) / t.range52.max) * 100).toFixed(1)}%
Distancia al mín. +{(((t.price - t.range52.min) / t.range52.min) * 100).toFixed(1)}%
); } // ─── Sub-tabs ───────────────────────────────────────────────────────────── function ResumenTab({ t, news, accent }) { return ( <>
Sobre {t.name}

{t.desc}

Consenso de analistas
4.2 / 5
{[ { label: 'Comprar fuerte', n: 18, pct: 60, cls: 'up' }, { label: 'Comprar', n: 8, pct: 27, cls: 'up' }, { label: 'Mantener', n: 3, pct: 10, cls: 'mid' }, { label: 'Vender', n: 1, pct: 3, cls: 'dn' }, ].map((r) => (
{r.label}
{r.n}
))}
Noticias recientes Ver todas →
{news.slice(0, 3).map((n, i) => )}
); } function NoticiasTab({ news }) { return (
Todas las noticias
{news.map((n, i) => (
{n.tag}
{n.headline}
{n.src} · {n.when}
))}
); } function FundTab({ t }) { const rows = [ ['Ingresos (TTM)', '$382.4B', '$391.0B', '+2.3%'], ['Beneficio neto', '$101.0B', '$99.8B', '−1.2%'], ['Margen operativo', '30.1%', '29.4%', '−70 pb'], ['Flujo de caja libre', '$96.4B', '$92.1B', '−4.5%'], ['Deuda total', '$108B', '$104B', '−3.7%'], ['ROE', '147%', '156%', '+900 pb'], ]; return (
Estado financiero · {t.name}
{rows.map((r) => ( ))}
Métrica20242023Var.
{r[0]} {r[1]} {r[2]} {r[3]}
); } function TecnicoTab({ t }) { const signals = [ { label: 'RSI (14)', val: '58.2', tag: 'Neutral', cls: 'mid' }, { label: 'MACD', val: '+1.42', tag: 'Compra', cls: 'up' }, { label: 'Media móvil 50d', val: '$208.40', tag: 'Compra', cls: 'up' }, { label: 'Media móvil 200d', val: '$192.10', tag: 'Compra fuerte', cls: 'up' }, { label: 'Bandas de Bollinger', val: 'Centro', tag: 'Neutral', cls: 'mid' }, { label: 'Estocástico', val: '72.1', tag: 'Sobrecomprado', cls: 'dn' }, ]; return (
Indicadores técnicos
{signals.map((s) => (
{s.label}
{s.val}
{s.tag}
))}
); } function PerfilTab({ t }) { return (
Descripción

{t.desc}

Datos clave
  • CEO{'M. Vázquez'}
  • Sede{'San Francisco, CA'}
  • Empleados{'164,200'}
  • Fundación{'1998'}
  • Web{t.name.toLowerCase().replace(/\s+/g,'')}.com
); } function NewsCard({ n, accent }) { const toneColors = { pos: 'var(--accent)', neg: 'var(--neg)', neutral: 'var(--c-cyan)' }; const c = toneColors[n.tone] || 'var(--accent)'; return (
{n.tag}

{n.headline}

{n.src} · {n.when}
); } // ─── Calculator ──────────────────────────────────────────────────────────── // Herramienta de análisis: dado un número de acciones (o monto a invertir), // muestra el valor actual y permite proyectar valor a un precio objetivo. function Calculator({ t, fmt }) { const [shares, setShares] = React.useState(100); const [target, setTarget] = React.useState(t.price); const [editing, setEditing] = React.useState('shares'); // 'shares' | 'amount' const [amount, setAmount] = React.useState(100 * t.price); // Estado local para inputs en texto — evita reformateo en cada tecla const [sharesText, setSharesText] = React.useState('100'); const [amountText, setAmountText] = React.useState(''); const [targetText, setTargetText] = React.useState(''); const [focusField, setFocusField] = React.useState(null); // Cambio de ticker: resetear React.useEffect(() => { setShares(100); setAmount(100 * t.price); setTarget(t.price); setEditing('shares'); setSharesText('100'); }, [t.symbol]); // Sincronización bidireccional React.useEffect(() => { if (editing === 'shares') setAmount(shares * t.price); }, [shares, t.price, editing]); React.useEffect(() => { if (editing === 'amount') setShares(amount / t.price); }, [amount, t.price, editing]); // Cuando NO está siendo editado, mostrar el valor formateado const sharesDisplay = focusField === 'shares' ? sharesText : Number.isInteger(shares) ? String(shares) : shares.toFixed(2); const amountDisplay = focusField === 'amount' ? amountText : fmt(amount); const targetDisplay = focusField === 'target' ? targetText : fmt(target); const currentValue = shares * t.price; const projectedValue = shares * target; const diff = projectedValue - currentValue; const diffPct = t.price > 0 ? ((target - t.price) / t.price) * 100 : 0; const isUp = diff >= 0; const presets = [ { label: '−10%', pct: -0.10 }, { label: 'Actual', pct: 0 }, { label: '+10%', pct: 0.10 }, { label: '+25%', pct: 0.25 }, { label: 'Máx. 52s', value: t.range52.max }, ]; const sliderMin = t.range52.min * 0.5; const sliderMax = t.range52.max * 1.5; const parseNum = (s) => { const cleaned = String(s).replace(/[^\d.]/g, ''); const n = Number(cleaned); return Number.isFinite(n) ? Math.max(0, n) : 0; }; return (

Calculadora

${fmt(t.price)} actual
{/* Inputs: acciones <-> inversión */}
{/* Valor actual destacado */}
Valor actual de la posición
${fmt(currentValue)}
{/* Proyección */}
Precio objetivo {diffPct >= 0 ? '+' : ''}{diffPct.toFixed(1)}%
$ { setFocusField('target'); setTargetText(target.toFixed(2)); }} onBlur={() => setFocusField(null)} onChange={(e) => { setTargetText(e.target.value); setTarget(parseNum(e.target.value)); }}/>
setTarget(Number(e.target.value))}/>
${fmt(sliderMin)} ${fmt(t.price)} ${fmt(sliderMax)}
{presets.map((p) => { const v = p.value != null ? p.value : t.price * (1 + p.pct); const active = Math.abs(target - v) < 0.01; return ( ); })}
Valor proyectado ${fmt(projectedValue)}
Diferencia {isUp ? '+' : '−'}${fmt(Math.abs(diff))}
Rentabilidad {diffPct >= 0 ? '+' : ''}{diffPct.toFixed(2)}%
); } window.StockDetail = StockDetail;