/* ========================================================= VALIDQ · TICKER DETAIL · PRO MODE MRK · Merck & Co. · score 58 · HC SWEET SPOT ========================================================= */ /* === D1-C4e.4 engine + analysis labels === */ /* === D1-C4e.3 lifecycle dot fix + residuals === */ /* === D1-C4e.2 motor v3 signal patterns === */ /* === D1-C4e.1 signal verdict + pattern translation === */ /* === D1-C4e JSX runtime i18n (EN-native + T() + DICT EN-ES) === */ const { useState, useEffect, useRef } = React; const T_DICT_EN_ES = {"Search another ticker — JNJ, KO, NESN.SW…": "Buscar otro ticker — JNJ, KO, NESN.SW…", "You need an account to analyse tickers. Free plan includes 5 analyses per month.": "Necesitas una cuenta para analizar tickers. El plan Gratuito incluye 5 análisis al mes.", "Intrinsic value vs current price": "Valor intrínseco vs precio actual", "Tactical & NLP signals": "Señales tácticas y NLP", "Sector sweet-spot": "Zona óptima del sector", "Composite score": "Puntuación compuesta", "Score breakdown": "Desglose de puntuación", "Sign in required": "Inicio de sesión requerido", "Current drivers": "Drivers actuales", "Intrinsic value": "Valor intrínseco", "Current price": "Precio actual", "Current score": "Puntuación actual", "Mapping error": "Error de mapeo", "Toggle theme": "Cambiar tema", "Back to home": "Volver al inicio", "Bad request": "Solicitud inválida", "Methodology": "Metodología", "Significance": "Significancia", "Agent hive": "Colmena de agentes", "Sweet spot": "Zona óptima", "Lifecycle": "Ciclo de vida", "Confidence": "Confianza", "Key stats": "Datos clave", "Analysing": "Analizando", "Sign out": "Cerrar sesión", "Sign in": "Iniciar sesión", "Account": "Cuenta", "Alerts": "Alertas", "Retry": "Reintentar", "Terms": "Términos", "Close": "Cerrar", "Upside": "Potencial", "Win rate": "Tasa de aciertos", "Agent": "Agente", "News": "Noticias", "Sell-side revisions 30d": "Revisiones del sell-side 30d", "Healthcare relative": "Relativo Sanidad", "Consensus rating": "Rating de consenso", "ETF inflows 30d": "Entradas ETF 30d", "Earnings momentum": "Momentum de beneficios", "Net Debt/EBITDA": "Deuda neta/EBITDA", "Moat (Damodaran)": "Foso (Damodaran)", "FY25 EPS guide": "Guía EPS FY25", "Operating margin": "Margen operativo", "Earnings yield": "Rentabilidad por beneficio", "Buyers/Sellers": "Compradores/Vendedores", "FCF yield TTM": "Rentabilidad FCF TTM", "Last 3 calls": "Últimas 3 calls", "Short interest": "Interés corto", "Switching cost": "Coste de cambio", "Sector NLP avg": "Media NLP del sector", "Brand equity": "Valor de marca", "Change vs Q1": "Cambio vs T1", "Yield curve": "Curva de tipos", "Sector cycle": "Ciclo del sector", "Avg maturity": "Vencimiento medio", "Avg target": "Precio objetivo medio", "Debt/Equity": "Deuda/Patrimonio", "CAPEX/Sales": "CAPEX/Ventas", "DCF upside": "Potencial DCF", "IP / patents": "PI / patentes", "Cost moat": "Foso de costes", "WC days": "Días WC", "Net 90d": "Neto 90d", "Threshold": "Umbral", "Prep tone": "Tono preparado", "Strong (Keytruda 2028)": "Fuerte (Keytruda 2028)", "Late-cycle defensive": "Defensivo de ciclo tardío", "Bullish-neutral": "Alcista-neutral", "+0.42 (bullish)": "+0.42 (alcista)", "4.2 (safe zone)": "4.2 (zona segura)", "−2.41 (clean)": "−2.41 (limpio)", "Buy (4.1/5)": "Compra (4.1/5)", "+0.05 (raise)": "+0.05 (sube)", "Medium-high": "Medio-alto", "0.04 (low)": "0.04 (bajo)", "Steepening": "Empinándose", "1.2% float": "1.2% del float", "8.4 years": "8.4 años", "Medium": "Medio", "Back to feed": "Volver al feed", "In watchlist": "En watchlist", "Add to watchlist": "Añadir a watchlist", "Re-analyse": "Reanalizar", "Analyse another": "Analizar otro", "analysed": "analizado", "cache 14m": "caché 14m", "calibrated against SSRN paper 6735820": "calibrado con paper SSRN 6735820", "vs SPX · 36 months": "vs SPX · 36 meses", "annualised": "anualizado", "positive cohorts": "cohortes positivas", "Backtest 2012–2014, 2017–2019 · 4 107 ticker-years": "Backtest 2012–2014, 2017–2019 · 4 107 años-ticker", "7 academic models · click for detail": "7 modelos académicos · clic para detalle", "7 agents · click for detail": "7 agentes · clic para detalle", "12-month horizon": "horizonte 12 meses", "Start-up": "Inicio", "Young Growth": "Crec. joven", "High Growth": "Crec. alto", "Mature G.": "Mad. crec.", "Mature Stable": "Madurez estable", "Decline": "Declive", "article": "artículo", "articles": "artículos", "active": "activas", "no alerts": "sin alertas", "No alerts yet.": "Aún no hay alertas.", "Earnings + score deltas will appear here.": "Variaciones de earnings y score aparecerán aquí.", "CAGR α": "TCAC α", "2-stage Mauboussin · WACC 7.2% · g 2.5%": "Mauboussin 2-etapas · WACC 7.2% · g 2.5%", "Accounting integrity": "Integridad contable", "Bankruptcy risk": "Riesgo de quiebra", "Business quality": "Calidad del negocio", "Valuation": "Valoración", "Debt structure": "Estructura de deuda", "Free Cash Flow Momentum": "Momentum de Free Cash Flow", "Guidance Reliability": "Fiabilidad de la guía", "Insider Activity": "Actividad de insiders", "ATLAS · Intangibles & Moat": "ATLAS · Intangibles y foso", "Sector timing": "Timing del sector", "CHRONOS · Cycle timing": "CHRONOS · Timing del ciclo", "CLIO · Business model": "CLIO · Modelo de negocio", "ARGOS · Consensus & flows": "ARGOS · Consenso y flujos", "HIGH": "ALTA", "MEDIUM": "MEDIA", "LOW": "BAJA", "Signal · quantitative verdict": "Señal · veredicto cuantitativo", "Alpha 3y": "Alfa 3a", "view detail": "ver detalle", "Valuation · reverse-DCF (Mauboussin)": "Valoración · reverse-DCF (Mauboussin)", "Valuation band": "Banda de valoración", "· overvalued": "· sobrevalorada", "· undervalued": "· infravalorada", "current": "actual", "fair value": "valor justo", "undervalued": "infravalorada", "fair": "justa", "expensive": "cara", "range": "rango", "inside": "dentro", "above range": "por encima", "below range": "por debajo", "EODHD · adjusted close": "EODHD · cierre ajustado", "My account": "Mi cuenta", "Profile & details": "Perfil y detalles", "Manage tickers": "Gestionar tickers", "History": "Historial", "Plan & billing": "Plan y facturación", "Help": "Ayuda", "Docs & support": "Docs y soporte", "this month": "este mes", "Layer": "Capa", "C1–C5 are the \"pure validation\" layers (SSRN paper 6735820). C6–C7 add NLP and intangibles signals. Recomputed after a new 10-K/10-Q or manual re-analysis.": "C1–C5 son las capas de \"validación pura\" (paper SSRN 6735820). C6–C7 añaden señales NLP e intangibles. Se recomputan tras un nuevo 10-K/10-Q o reanálisis manual.", "current score": "score actual", "Updated 14m ago · EODHD + SEC EDGAR data. Re-evaluated every 24h or after a new 10-Q/8-K.": "Actualizado hace 14m · Datos EODHD + SEC EDGAR. Re-evaluado cada 24h o tras nuevo 10-Q/8-K.", "confidence": "confianza", "Monthly quota reached": "Cuota mensual alcanzada", "You've used all your analyses this month. Upgrade your plan to continue.": "Has usado todos tus análisis del mes. Sube de plan para continuar.", "Plan upgrade required": "Mejora de plan requerida", "This feature requires a higher plan": "Esta función requiere un plan superior", "Ticker not found": "Ticker no encontrado", "Temporary error": "Error temporal", "Please try again": "Inténtalo de nuevo", "Watchlist error": "Error de watchlist", "removed from watchlist": "eliminado de watchlist", "added to watchlist": "añadido a watchlist", "AVOID": "EVITAR", "BUY": "COMPRAR", "WAIT": "ESPERAR", "REVIEW": "REVISAR", "QUALITY TRAP": "TRAMPA DE CALIDAD", "VALUE TRAP": "TRAMPA DE VALOR", "MOMENTUM TRAP": "TRAMPA DE MOMENTUM", "DEEP VALUE": "VALOR PROFUNDO", "GROWTH TRAP": "TRAMPA DE CRECIMIENTO", "QUALITY MISS": "CALIDAD INSUFICIENTE", "VALUE MISS": "INFRAVALORACIÓN AUSENTE", "HIGH QUALITY": "ALTA CALIDAD", "WIDE MOAT": "FOSO AMPLIO", "FAIR VALUE": "VALOR JUSTO", "CLEAR BUY": "COMPRA CLARA", "DEEP VALUE TRAP": "TRAMPA VALOR PROFUNDO", "PREMIUM PRICED": "PRECIO PREMIUM", "VALUE SWEET SPOT": "SWEET SPOT DE VALOR", "VALUE SWEET SPOT HC": "SWEET SPOT DE VALOR HC", "VALUE SWEET SPOT LITE": "SWEET SPOT DE VALOR LITE", "HC SWEET SPOT": "SWEET SPOT HC", "1 year · USD": "1 año · USD", "52w position": "Posición 52s", "GICS industry": "Industria GICS", "Engine": "Motor", "Analysis": "Análisis", "Engine v6.1 · 7 layers · 9 agents": "Motor v6.1 · 7 capas · 9 agentes", "Investment thesis": "Tesis de inversión", "Powered by": "Generado por", "Plan": "Plan"}; const T = (s) => { if (typeof s !== "string") return s; try { const lang = (typeof localStorage !== "undefined" && localStorage.getItem("vq_lang")) || "es"; if (lang === "es" && T_DICT_EN_ES[s]) return T_DICT_EN_ES[s]; } catch (e) {} return s; }; /* ---------- Lucide-style icons (inline SVG, line-only) ---------- */ const I = ({ d, size = 16, sw = 1.6, children, ...rest }) => {children || } ; const Icon = { ArrowLeft: (p) => , Search: (p) => , Sun: (p) => , Moon: (p) => , Star: (p) => , Refresh: (p) => , Share: (p) => , Info: (p) => , Check: (p) => , X: (p) => , ChevronDown: (p) => , Flask: (p) => , Scale: (p) => , Gem: (p) => , Tag: (p) => , Landmark: (p) => , Home: (p) => , Wave: (p) => , Trending: (p) => , Cpu: (p) => , User: (p) => , Eye: (p) => , Mic: (p) => , Clock: (p) => , Layers: (p) => , Calendar: (p) => , Bell: (p) => , Newspaper: (p) => , Activity: (p) => }; /* ---------- DEMO DATA · MRK ---------- */ const TICKER = { symbol: "MRK", market: "NYSE", flag: "🇺🇸", name: "Merck & Co., Inc.", sector: "Healthcare", sub: "Pharmaceuticals", lifecycle: "Mature Stable", lifecycleConfidence: 79, price: 111.38, change: 0.82, changePct: 0.74, currency: "USD", asOf: "May 15, 2026 · 19:25 CET", lastAnalysis: "14m ago", score_display: 58, score_c1c5: 58, score_with_agents: 63, sectorSweetSpot: { lo: 45, hi: 58, label: "Healthcare · Mature stable" }, signal: { verdict: "BUY", pattern: "HC SWEET SPOT", confidence: 3, confLabel: T("HIGH"), desc: "Healthcare mature-stable score 45–58. The engine identifies this pattern with empirical magnitude documented across 6 cohorts (2012–2014, 2017–2019).", alpha3y: "+18.6pp", cagrAlpha: "+6.20pp", winRate: "6/6", pValue: "p<0.001" }, layers: [ { code: "C1", name: T("Accounting integrity"), icon: "Flask", pass: true, score: null, max: null, detail: "Beneish M-Score −2.41 · no manipulation detected", methodology: "Beneish M-Score detects accounting manipulation from 8 ratios (DSRI, GMI, AQI, SGI, DEPI, SGAI, LVGI, TATA). Threshold −1.78. Validated across >70 SEC cases.", drivers: [["M-Score", T("−2.41 (clean)")], ["DSRI", "1.04"], ["AQI", "0.98"], ["TATA", "−0.04"], [T("Threshold"), "−1.78"]] }, { code: "C2", name: T("Bankruptcy risk"), icon: "Scale", pass: true, score: null, max: null, detail: "Altman Z = 4.2 · Ohlson P = 0.04", methodology: "Altman Z-Score (5 ratios) + Ohlson O-Score as cross-check. Z>3 = safe zone. P<0.5 = low probability of bankruptcy within 12 months.", drivers: [["Altman Z", T("4.2 (safe zone)")], ["Ohlson P", T("0.04 (low)")], ["WC/TA", "0.18"], ["RE/TA", "0.41"], ["EBIT/TA", "0.16"]] }, { code: "C3", name: T("Business quality"), icon: "Gem", pass: null, score: 28, max: 35, detail: "Piotroski 7/9 · Moat 16/22 · ROIC 18.4%", methodology: "Combines Piotroski F-Score (9 binary signals across profitability/leverage/efficiency), Damodaran moat scoring and ROIC vs WACC. Weighted by persistence.", drivers: [["Piotroski F", "7/9"], [T("Moat (Damodaran)"), "16/22"], ["ROIC", "18.4%"], ["ROIC − WACC", "+11.2pp"], [T("Operating margin"), "31.5%"]] }, { code: "C4", name: T("Valuation"), icon: "Tag", pass: null, score: 14, max: 25, warn: true, detail: "P/E 18.4× · DCF +12% upside · EV/EBITDA 14×", methodology: "Triangulation: relative multiples (P/E, EV/EBITDA, P/B) vs sector + 2-stage reverse DCF (Mauboussin). Adjusted for lifecycle stage.", drivers: [["P/E TTM", "18.4× (sector 19.2×)"], ["EV/EBITDA", "14.1×"], [T("DCF upside"), "+12.0%"], ["P/B", "5.2×"], [T("Earnings yield"), "5.4%"]] }, { code: "C5", name: T("Debt structure"), icon: "Landmark", pass: null, score: 18, max: 25, detail: "EV/EBITDA 14× · Coverage 18× · ND/EBITDA 1.4×", methodology: "Assesses leverage (ND/EBITDA), debt-service capacity (interest coverage) and maturity ladder. Penalises short-term maturity concentration.", drivers: [[T("Net Debt/EBITDA"), "1.4×"], ["Interest coverage", "18×"], [T("Debt/Equity"), "0.62"], [T("Avg maturity"), T("8.4 years")], ["Credit rating", "AA-"]] }, { code: "C6", name: "ATLAS · Intangibles", icon: "Home", pass: null, score: 12, max: 20, detail: "5 signals · solid moat · strong IP", methodology: "NLP + structured agent over 10-K filings. 5 dimensions: IP, switching costs, network effects, scale moat, brand. Same engine as the ATLAS agent, integrated as a layer.", drivers: [[T("IP / patents"), T("Strong (Keytruda 2028)")], [T("Switching cost"), T("Medium")], [T("Brand equity"), "8.4 / top 25%"], ["Network effects", "Low"], [T("Cost moat"), T("Medium-high")]] }, { code: "C7", name: "HERMES · NLP", icon: "Wave", pass: null, score: 14, max: 30, warn: true, detail: "Management tone neutral-bullish · 3 earnings calls", methodology: "NLP over transcripts (Loughran-McDonald + in-house finetune). Compares prepared vs Q&A tone, hedging, guidance clarity. Last 3 calls weighted.", drivers: [[T("Prep tone"), T("+0.42 (bullish)")], ["Q&A tone", "+0.18 (neutral)"], ["Hedging", "low"], [T("Last 3 calls"), T("Bullish-neutral")], [T("Sector NLP avg"), "+0.12"]] }], agents: [ { name: "FCF-MOM", v: "+5", d: "/15", sign: "pos", tag: "CFO yield expansion", full: T("Free Cash Flow Momentum"), methodology: "Detects FCF / CFO yield acceleration over 4–8 quarters. Signals margin expansion and pricing power.", drivers: [[T("FCF yield TTM"), "5.20%"], ["FCF yield (-1Y)", "4.45%"], [T("CAPEX/Sales"), "-12 bp QoQ"], [T("WC days"), "-3 d YoY"]] }, { name: "GUIDANCE", v: "+3", d: "/15", sign: "pos", tag: "No FY25 cuts", full: T("Guidance Reliability"), methodology: "Evaluates the consistency of quarterly guidance and consensus revisions. Penalises recent cuts.", drivers: [[T("FY25 EPS guide"), "$8.10-8.25"], [T("Change vs Q1"), T("+0.05 (raise)")], ["Beat rate (8Q)", "7/8"], [T("Sell-side revisions 30d"), "+0.4%"]] }, { name: "INSIDER", v: "−2", d: "/12", sign: "neg", tag: "Moderate selling", full: T("Insider Activity"), methodology: "SEC Form 4. Net buying / selling weighted by management-signal vs stock-options-vesting noise.", drivers: [[T("Net 90d"), "-$4.2M"], [T("Buyers/Sellers"), "1 / 4"], ["% executive signal", "38%"], ["Cluster activity", "No"]] }, { name: "ATLAS", v: "+12", d: "/20", sign: "pos", tag: "Moat · 5 signals", full: T("ATLAS · Intangibles & Moat"), methodology: "NLP + structured over 10-K. Detects IP, switching costs, network effects, scale, brand. Damodaran moat scoring.", drivers: [[T("IP / patents"), T("Strong (Keytruda 2028)")], [T("Switching cost"), T("Medium")], [T("Brand equity"), "8.4 / top 25%"], ["Network effects", "Low"], [T("Cost moat"), T("Medium-high")]] }, { name: "HERMES", v: "+14", d: "/30", sign: "pos", tag: "Bullish neutral", full: "HERMES · NLP earnings calls", methodology: "Sentiment + management tone over earnings-call transcripts (Loughran-McDonald + in-house finetune). Compares Q&A vs prepared remarks.", drivers: [[T("Prep tone"), T("+0.42 (bullish)")], ["Q&A tone", "+0.18 (neutral)"], ["Hedging frequency", "low"], [T("Last 3 calls"), T("Bullish-neutral")]] }, { name: "CHRONOS", v: "+3", d: "/10", sign: "amb", tag: "Cycle neutral", full: T("CHRONOS · Cycle timing"), methodology: "Macro + sector cycle positioning (yield curve, PMI, sector rotation). Signals timing relative to the cycle.", drivers: [[T("Sector cycle"), T("Late-cycle defensive")], [T("Yield curve"), T("Steepening")], [T("Healthcare relative"), "+0.3 sigma"], [T("Earnings momentum"), "Neutral"]] }, { name: "ARGOS", v: "+4", d: "/8", sign: "pos", tag: "Consensus buy", full: T("ARGOS · Consensus & flows"), methodology: "Wisdom of crowds: sell-side consensus + ETF flows + short interest. Filters pure-momentum noise.", drivers: [[T("Consensus rating"), T("Buy (4.1/5)")], [T("Avg target"), "$132 (+18%)"], [T("Short interest"), T("1.2% float")], [T("ETF inflows 30d"), "+$240M"]] }], dcf: { current: 111.38, intrinsic: 124.77, upside: 12.0, method: T("2-stage Mauboussin · WACC 7.2% · g 2.5%"), range: { lo: 90, hi: 150 } }, stats: [ { l: "Market cap", v: "$282.4B" }, { l: "P/E (TTM)", v: "18.4×" }, { l: "EV/EBITDA", v: "14.1×" }, { l: "ROIC", v: "18.4%", g: true }, { l: "FCF yield", v: "5.2%", g: true }, { l: "Dividend yield", v: "2.94%" }, { l: "Payout ratio", v: "42%" }, { l: "Beta (5Y)", v: "0.41" }, { l: T("GICS industry"), v: "Pharmaceuticals" }, { l: T("Brand equity"), v: "8.4 · top 25%" }, { l: T("Engine"), v: "v3.2" }, { l: T("Analysis"), v: "248 ms" }], news: [ { src: "Reuters", time: "2h", title: "Trump says unaware of plan to fire FDA commissioner Marty Makary" }, { src: "Bloomberg", time: "5h", title: "How Hantavirus made Moderna's mRNA platform a target for Merck partnership talks" }, { src: "WSJ", time: "1d", title: "Merck faces Gardasil trial after plaintiffs allege concealed side effects" }, { src: "FT", time: "2d", title: "Keytruda subcutaneous formulation cleared for late-2026 launch in EU" }], alerts: [ { kind: "up", text: "Score up +2 points vs previous analysis", meta: "14 days ago · 56 → 58" }, { kind: "info", text: "Next earnings: July 18 · pre-market", meta: "Q2 2026 · estimated EPS $2.21" }] }; /* ---------- TICKER SEARCH (autocomplete-ready) ---------- */ const TICKER_DB = [ { sym: "MRK", name: "Merck & Co.", mkt: "NYSE" }, { sym: "JNJ", name: "Johnson & Johnson", mkt: "NYSE" }, { sym: "PFE", name: "Pfizer Inc.", mkt: "NYSE" }, { sym: "LLY", name: "Eli Lilly", mkt: "NYSE" }, { sym: "NVO", name: "Novo Nordisk", mkt: "NYSE" }, { sym: "KO", name: "Coca-Cola", mkt: "NYSE" }, { sym: "PEP", name: "PepsiCo", mkt: "NASDAQ" }, { sym: "AAPL", name: "Apple Inc.", mkt: "NASDAQ" }, { sym: "MSFT", name: "Microsoft", mkt: "NASDAQ" }, { sym: "GOOGL", name: "Alphabet (A)", mkt: "NASDAQ" }, { sym: "NESN.SW", name: "Nestlé", mkt: "SWX" }, { sym: "ROG.SW", name: "Roche Holding", mkt: "SWX" }, { sym: "NOVN.SW", name: "Novartis", mkt: "SWX" }, { sym: "SAN.PA", name: "Sanofi", mkt: "EPA" }, { sym: "ITX.MC", name: "Inditex", mkt: "BME" }, { sym: "IBE.MC", name: "Iberdrola", mkt: "BME" }, { sym: "BRK.B", name: "Berkshire Hathaway", mkt: "NYSE" }, { sym: "V", name: "Visa", mkt: "NYSE" }, { sym: "MA", name: "Mastercard", mkt: "NYSE" }, { sym: "PG", name: "Procter & Gamble", mkt: "NYSE" } ]; // TODO: replace with /api/tickers/search?q= (DB) async function searchTickers(q) { const Q = q.trim().toUpperCase(); if (!Q) return []; return TICKER_DB.filter(t => t.sym.includes(Q) || t.name.toUpperCase().includes(Q)).slice(0, 8); } function TickerSearch() { const [q, setQ] = useState(""); const [open, setOpen] = useState(false); const [results, setResults] = useState([]); const [active, setActive] = useState(0); const wrapRef = useRef(null); useEffect(() => { let alive = true; searchTickers(q).then(r => alive && setResults(r)); return () => { alive = false; }; }, [q]); useEffect(() => { const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); const showList = open && q.length > 0; const onKey = (e) => { if (!showList) return; if (e.key === "ArrowDown") { e.preventDefault(); setActive((active + 1) % Math.max(1, results.length)); } if (e.key === "ArrowUp") { e.preventDefault(); setActive((active - 1 + results.length) % Math.max(1, results.length)); } if (e.key === "Enter" && results[active]) { setQ(results[active].sym); setOpen(false); } if (e.key === "Escape") setOpen(false); }; return (
{ setQ(e.target.value); setOpen(true); setActive(0); }} onFocus={() => setOpen(true)} onKeyDown={onKey} /> ⌘K {showList && (
{results.length === 0 &&
No matches in the database
} {results.map((t, i) => ( ))}
)}
); } /* ---------- USER MENU ---------- */ function UserMenu() { const [open, setOpen] = useState(false); const [me, setMe] = useState(null); const ref = useRef(null); useEffect(() => { const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); // Fetch real user info useEffect(() => { if (!window.vqApi || !vqApi.isLoggedIn()) return; vqApi.me() .then(u => setMe(u)) .catch(() => setMe(null)); }, []); // If not logged → show Sign in button if (!window.vqApi || !vqApi.isLoggedIn()){ return ( ); } const email = (me && me.email) || "..."; const userPart = email.split("@")[0].split(".")[0]; const displayName = userPart.charAt(0).toUpperCase() + userPart.slice(1); const avatarLetter = (email[0] || "?").toUpperCase(); const plan = (me && me.plan) || "—"; const queriesUsed = (me && me.queries_this_month) || 0; const queriesLimit = (me && me.queries_limit) || "∞"; const handleSignOut = () => { if (window.vqApi) vqApi.logout("/"); }; const items = [ { l: T("My account"), s: T("Profile & details"), href: "/cuenta.html" }, { l: "Watchlist", s: T("Manage tickers"), href: "/home.html#watchlist" }, { l: T("History"), s: `${queriesUsed} analyses this month`, href: "/home.html#history" }, { l: T("Plan & billing"), s: `Plan ${plan}`, href: "/billing.html" }, { l: T("Help"), s: T("Docs & support"), href: "/legal/metodologia.html" } ]; return (
{open && (
{avatarLetter}
{displayName}
{email}
Plan {plan} · {queriesUsed}/{queriesLimit} {T("this month")}
{items.map(it => ( {it.l} {it.s} ))}
)}
); } /* ---------- NAV ---------- */ function Nav({ theme, setTheme }) { return ( );} /* ---------- TICKER HEADER ---------- */ function TickerHeader({ t, watch, setWatch }) { const pos = t.change >= 0; return ( <> {T("Back to feed")}
{t.flag} {t.symbol} {t.market} · {t.name}
{t.sector} · {t.sub} {t.lifecycle} Engine v3.2 {T("cache 14m")}
${t.price.toFixed(2)}
{t.currency} · {t.asOf} · {T("analysed")} {t.lastAnalysis}
); } /* ---------- SCORE HERO (Pro) ---------- */ function ScoreHero({ t }) { const score = t.score_c1c5; const tone = score >= 55 ? "" : score >= 40 ? "amber" : "red"; const ss = t.sectorSweetSpot; // Sweet spot is calibrated on C1-C5 pure score, so the pin uses score_c1c5 const pinScore = t.score_c1c5; const pinPct = pinScore; // axis 0..100 const ssLeft = ss.lo; const ssWidth = ss.hi - ss.lo; return (
{score}
/ 100
{T("Sweet spot")}
C1–C5 pure
{T("Composite score")} {T("calibrated against SSRN paper 6735820")}
{T("Sector sweet-spot")}
{ss.label} {T("range")} {ss.lo}–{ss.hi} · {ss.position === "in" ? "inside ✓" : ss.position === "above" ? "above range ⚠" : ss.position === "below" ? "below range ⚠" : "—"}
{ss.lo} {ss.hi}
020406080100
{/* S12_FIX: bloque sh-split eliminado (redundante con resto del hero) */}
{/* S12_MNEMO_UI_v1: MNEMO narrative block */} {t.narrative && t.narrative.available && (t.narrative.textEs || t.narrative.textEn) && (
{T("Investment thesis")}
{'"' + t.narrative.oneLiner + '"'}

{t.narrative.textEs || t.narrative.textEn}

{T("Plan")} · {t.narrative.plan} · {t.narrative.textEs ? "ES" : "EN"} · {t.narrative.lifecycleStage} {t.narrative.degraded && ( <> · FALLBACK )}
{T("Powered by")} MNEMO
)}
); } /* ---------- SIGNAL ---------- */ function SignalCard({ t }) { const s = t.signal; const dots = Array.from({ length: 3 }).map((_, i) => ); return (
{T("Signal · quantitative verdict")}
{T(s.verdict)} · {T(s.pattern)}
{T("confidence")} {dots} {s.confLabel}

{s.desc}

{T("Alpha 3y")}
{s.alpha3y}
{T("vs SPX · 36 months")}
{T("CAGR α")}
{s.cagrAlpha}
{T("annualised")}
{T("Win rate")}
{s.winRate}
{T("positive cohorts")}
{T("Significance")}
{s.pValue}
95% CI · t-test
{T("Backtest 2012–2014, 2017–2019 · 4 107 ticker-years")} ·{" "} SSRN paper 6735820 ↗
); } /* ---------- LAYERS C1-C7 ---------- */ function LayersCard({ layers }) { const [open, setOpen] = useState(null); return (
Analysis · layers C1–C7
{T("Score breakdown")}
{T("7 academic models · click for detail")}
{layers.map((L) => { const isPass = L.pass === true; const pct = L.score != null ? Math.round(L.score / L.max * 100) : 100; const cls = "layer layer-btn " + (isPass ? "pass " : "") + (L.warn ? "warn " : ""); const Ico = Icon[L.icon] || Icon.Layers; return ( ); })}
{open && setOpen(null)} />}
); } function LayerModal({ layer, onClose }) { useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, [onClose]); const L = layer; const isPass = L.pass === true; const Ico = Icon[L.icon] || Icon.Layers; const pct = L.score != null ? Math.round(L.score / L.max * 100) : 100; return (
e.stopPropagation()} role="dialog" aria-modal="true">
{T("Layer")} {L.code}
{L.name}
{isPass ? PASS : <>{T("Current score")} {L.score}/{L.max} ({pct}%)}
{T("Methodology")}

{L.methodology}

{T("Current drivers")}
{L.drivers.map(([k, v], i) => (
{k} {v}
))}
{T("C1–C5 are the \"pure validation\" layers (SSRN paper 6735820). C6–C7 add NLP and intangibles signals. Recomputed after a new 10-K/10-Q or manual re-analysis.")}
); } /* ---------- AGENTS HIVE ---------- */ function AgentsCard({ agents }) { const [open, setOpen] = useState(null); return (
{T("Agent hive")}
{T("Tactical & NLP signals")}
{T("7 agents · click for detail")}
{agents.map((a) => )}
{open && setOpen(null)} />}
); } function AgentModal({ agent, onClose }) { useEffect(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, [onClose]); const a = agent; return (
e.stopPropagation()} role="dialog" aria-modal="true">
{T("Agent")}
{a.full}
{a.name} · {T("current score")} {a.v}{a.d}
{T("Methodology")}

{a.methodology}

{T("Current drivers")}
{a.drivers.map(([k, v], i) => (
{k} {v}
))}
{T("Updated 14m ago · EODHD + SEC EDGAR data. Re-evaluated every 24h or after a new 10-Q/8-K.")}
); } /* ---------- DCF ---------- */ function DcfCard({ d }) { // Build a scale that ALWAYS includes current price + intrinsic + range const allVals = [d.range.lo, d.range.hi, d.current, d.intrinsic].filter(v => v && v > 0); const rawMin = Math.min(...allVals); const rawMax = Math.max(...allVals); // Add 8% padding on each side so pins never touch the edges const padding = (rawMax - rawMin) * 0.08 || 1; const scaleMin = rawMin - padding; const scaleMax = rawMax + padding; const pct = (v) => Math.max(2, Math.min(98, (v - scaleMin) / (scaleMax - scaleMin) * 100)); const lo = d.range.lo, hi = d.range.hi; // Verdict: is the stock cheap or expensive vs intrinsic? const isOvervalued = d.current > d.intrinsic; return (
{T("Valuation · reverse-DCF (Mauboussin)")}
{T("Intrinsic value vs current price")}
{d.method}
{T("Current price")}
${d.current.toFixed(2)}
USD · spot
{T("Intrinsic value")}
${d.intrinsic.toFixed(2)}
2-stage · WACC 7.2%
{T("Upside")}
= 0 ? "g" : "neg")}>{d.upside >= 0 ? "+" : ""}{d.upside.toFixed(1)}%
{T("12-month horizon")}
{T("Valuation band")} {isOvervalued ? T("· overvalued") : T("· undervalued")} bear ${lo.toFixed(0)} — bull ${hi.toFixed(0)}
{/* Range zone bear-bull as a shaded band */}
{/* Current price pin - label BELOW */}
${d.current.toFixed(2)} {T("current")}
{/* Intrinsic pin - label ABOVE */}
${d.intrinsic.toFixed(2)} {T("fair value")}
{T("undervalued")}{T("fair")}{T("expensive")}
); } /* ---------- PRICE CHART (interactive) ---------- */ function PriceChart({ ticker, currentPrice }) { const ranges = ["1M", "3M", "6M", "1Y", "3Y", "5Y"]; const [active, setActive] = useState("1Y"); const [hover, setHover] = useState(null); const wrapRef = useRef(null); // Range config: days span + number of points const rangeCfg = { "1M": { days: 30, n: 30, vol: 0.6 }, "3M": { days: 90, n: 45, vol: 0.9 }, "6M": { days: 180, n: 60, vol: 1.1 }, "1Y": { days: 365, n: 80, vol: 1.4 }, "3Y": { days: 1095, n: 120, vol: 2.2 }, "5Y": { days: 1825, n: 150, vol: 3.0 } }; const cfg = rangeCfg[active] || rangeCfg["1Y"]; // Deterministic series ending at currentPrice (so the chart "lands" on real price) const endPrice = currentPrice || 100; const pts = []; let y = endPrice * 0.78; // start ~22% below current for (let i = 0; i < cfg.n; i++) { y += Math.sin(i * 0.42) * cfg.vol + Math.cos(i * 0.17) * (cfg.vol * 0.6) + (endPrice * 0.22 / cfg.n); pts.push(y); } // Force last point to be the real current price if (pts.length) pts[pts.length - 1] = endPrice; const now = new Date(); const startDate = new Date(now.getTime() - cfg.days * 86400000); const dayStep = cfg.days / Math.max(1, pts.length - 1); const dates = pts.map((_, i) => new Date(startDate.getTime() + i * dayStep * 86400000)); const min = Math.min(...pts), max = Math.max(...pts); const W = 800, H = 200, pad = 8; const norm = (v) => H - pad - (v - min) / (max - min) * (H - pad * 2); const stepX = (W - pad * 2) / (pts.length - 1); const xAt = (i) => pad + i * stepX; const path = pts.map((v, i) => (i === 0 ? "M" : "L") + xAt(i).toFixed(2) + " " + norm(v).toFixed(2)).join(" "); const area = path + ` L ${xAt(pts.length - 1)} ${H} L ${pad} ${H} Z`; const onMove = (e) => { const wrap = wrapRef.current; if (!wrap) return; const r = wrap.getBoundingClientRect(); const x = e.clientX - r.left; const ratio = Math.max(0, Math.min(1, x / r.width)); const i = Math.round(ratio * (pts.length - 1)); setHover({ i, xPct: (xAt(i) / W) * 100, yPct: (norm(pts[i]) / H) * 100 }); }; const onLeave = () => setHover(null); const fmtDate = (d) => d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }); const hovDate = hover ? dates[hover.i] : null; const hovPx = hover ? pts[hover.i] : null; return (
Price
{T("1 year · USD")}
{T("EODHD · adjusted close")}
{ranges.map((r) => )}
{hover && (
{fmtDate(hovDate)} ${hovPx.toFixed(2)}
)}
{[0.25, 0.5, 0.75].map((g, i) => )} {hover && ( )} {hover && (
{fmtDate(hovDate)}
${hovPx.toFixed(2)}
)}
May 2025AugNovFeb 2026May 2026
); } /* ---------- SIDEBAR cards ---------- */ function LifecycleCard({ t }) { const stages = [T("Start-up"), T("Young Growth"), T("High Growth"), T("Mature G."), T("Mature Stable"), T("Decline")]; const activeIdx = 4; return (
{T("Lifecycle")}
Damodaran
{stages.map((s, i) =>
{s}
)}
Established company with stable growth, predictable dividend and low operational volatility.
{T("Confidence")} {t.lifecycleConfidence}% · {T("HIGH")}
); } function StatsCard({ stats }) { return (
{T("Key stats")}
EODHD · TTM
{stats.map((s) =>
{s.l} {s.v}
)}
); } function publisherFromUrl(url){ if (!url) return ""; try { const host = new URL(url).hostname.replace(/^www\./, ""); // Map common domains to friendly publisher names const map = { "finance.yahoo.com": "Yahoo Finance", "yahoo.com": "Yahoo Finance", "reuters.com": "Reuters", "bloomberg.com": "Bloomberg", "cnbc.com": "CNBC", "marketwatch.com": "MarketWatch", "investing.com": "Investing.com", "seekingalpha.com": "Seeking Alpha", "fool.com": "Motley Fool", "barrons.com": "Barron's", "ft.com": "Financial Times", "wsj.com": "WSJ", "benzinga.com": "Benzinga", "thestreet.com": "TheStreet", "businesswire.com": "Business Wire", "prnewswire.com": "PR Newswire", "globenewswire.com": "GlobeNewswire" }; return map[host] || host.split(".")[0].charAt(0).toUpperCase() + host.split(".")[0].slice(1); } catch(e){ return ""; } } function newsDateStr(iso){ if (!iso) return ""; try { const d = new Date(iso); const ms = Date.now() - d.getTime(); const h = Math.floor(ms / 3600000); if (h < 1) return Math.max(1, Math.floor(ms/60000)) + "m ago"; if (h < 24) return h + "h ago"; const days = Math.floor(h / 24); if (days < 7) return days + "d ago"; return d.toLocaleDateString("en-GB", { day: "numeric", month: "short" }); } catch(e){ return ""; } } function NewsCard({ news }) { return (
{T("News")}
{news.length} {news.length === 1 ? T("article") : T("articles")}
{news.length === 0 ? (
No recent news.
) : ( news.map((n, i) => { const publisher = publisherFromUrl(n.url); const when = newsDateStr(n.date); return (
{publisher && {publisher}} {publisher && when && ·} {when && {when}}
{n.title}
); }) )}
); } function AlertsCard({ alerts }) { const count = alerts.length; return (
{T("Alerts")}
{count > 0 ? `${count} ${T("active")}` : T("no alerts")}
{count === 0 ? (
{T("No alerts yet.")}
{T("Earnings + score deltas will appear here.")}
) : ( alerts.map((a, i) => { const icon = a.kind === "up" ? : a.kind === "down" ? : ; const cls = "alert-item " + (a.kind === "up" ? "up" : a.kind === "down" ? "down" : ""); return (
{icon}
{a.text} {a.meta && {a.meta}}
); }) )}
); } /* ---------- DISCLAIMER ---------- */ function Disclaimer() { return (
Validq is a quantitative analysis tool. It does not constitute personalised financial advice or a buy/sell recommendation. Investment decisions are the user's responsibility. MiFID II compliant. Data sourced from EODHD/SEC EDGAR. Backtest results do not guarantee future outcomes.{" "} {T("Methodology")} · {T("Terms")}.
); } /* ---------- APP ---------- */ /* ========================================================= MAPPING: backend /analyze/{ticker} → t (component shape) ========================================================= */ function flagForMarket(ticker){ const m = {".US":"🇺🇸",".SW":"🇨🇭",".MC":"🇪🇸",".PA":"🇫🇷",".DE":"🇩🇪",".L":"🇬🇧",".MI":"🇮🇹",".AS":"🇳🇱",".CO":"🇩🇰",".BR":"🇧🇪",".HE":"🇫🇮",".OL":"🇳🇴",".ST":"🇸🇪",".LS":"🇵🇹",".HK":"🇭🇰",".TO":"🇨🇦",".AX":"🇦🇺",".SA":"🇧🇷",".T":"🇯🇵",".NS":"🇮🇳"}; if (!ticker) return "🌐"; for (const k in m){ if (ticker.endsWith(k)) return m[k]; } return "🌐"; } function currencyFor(ticker){ const m = {".US":"USD",".SW":"CHF",".MC":"EUR",".PA":"EUR",".DE":"EUR",".L":"GBP",".MI":"EUR",".AS":"EUR",".CO":"DKK",".BR":"EUR",".HE":"EUR",".OL":"NOK",".ST":"SEK",".LS":"EUR",".HK":"HKD",".TO":"CAD",".AX":"AUD",".SA":"BRL",".T":"JPY",".NS":"INR"}; if (!ticker) return "USD"; for (const k in m){ if (ticker.endsWith(k)) return m[k]; } return "USD"; } function symbolFromTicker(t){ return t ? t.split(".")[0] : ""; } function marketCodeFromTicker(t){ if (!t) return ""; const ext = t.split(".")[1] || ""; const map = {"US":"NYSE","SW":"SIX","MC":"BME","PA":"EPA","DE":"XETRA","L":"LSE","MI":"BIT","AS":"AMS","HK":"HKEX","TO":"TSX","AX":"ASX","T":"TSE","NS":"NSE"}; return map[ext] || ext; } function timeAgoStr(iso){ if (!iso) return "—"; try { const ms = Date.now() - new Date(iso).getTime(); const mins = Math.floor(ms/60000); if (mins<60) return mins + "m ago"; const h = Math.floor(mins/60); if (h<24) return h + "h ago"; const d = Math.floor(h/24); return d + "d ago"; } catch(e){ return "—"; } } function parseSignal(sig){ if (!sig) return {verdict:"REVIEW", pattern:""}; const parts = sig.replace(/_/g," ").trim().split(/ +/); if (parts.length === 0) return {verdict:"REVIEW", pattern:""}; const verdictMap = { "COMPRA":"BUY","COMPRA_CLARA":"BUY","BUY":"BUY", "EVITAR":"AVOID","AVOID":"AVOID","NO_INVERTIR":"AVOID", "ESPERAR":"WAIT","ESPERAR_MEJOR_PRECIO":"WAIT","WAIT":"WAIT", "REVISAR":"REVIEW","REVIEW":"REVIEW" }; const first = parts[0].toUpperCase(); return { verdict: verdictMap[first] || first, pattern: parts.slice(1).join(" ").toUpperCase() }; } // Constants for layer/agent metadata const LAYER_META = { "1": {code:"C1", name:T("Accounting integrity"), icon:"Flask", methodology:"Beneish M-Score detects accounting manipulation from 8 ratios (DSRI, GMI, AQI, SGI, DEPI, SGAI, LVGI, TATA). Threshold −1.78. Validated across >70 SEC cases."}, "2": {code:"C2", name:T("Bankruptcy risk"), icon:"Scale", methodology:"Altman Z-Score (5 ratios) + Ohlson O-Score as cross-check. Z>3 = safe zone. P<0.5 = low probability of bankruptcy within 12 months."}, "3": {code:"C3", name:T("Business quality"), icon:"Gem", methodology:"Combines Piotroski F-Score (9 binary signals across profitability/leverage/efficiency), Damodaran moat scoring and ROIC vs WACC. Weighted by persistence."}, "4": {code:"C4", name:T("Valuation"), icon:"Tag", methodology:"Triangulation: relative multiples (P/E, EV/EBITDA, P/B) vs sector + 2-stage reverse DCF (Mauboussin). Adjusted for lifecycle stage."}, "5": {code:"C5", name:T("Debt structure"), icon:"Landmark", methodology:"Assesses leverage (ND/EBITDA), debt-service capacity (interest coverage) and maturity ladder. Penalises short-term maturity concentration."} }; const AGENT_META = { fcf_momentum: {name:"FCF-MOM", full:T("Free Cash Flow Momentum"), max:15, methodology:"Detects FCF / CFO yield acceleration over 4–8 quarters. Signals margin expansion and pricing power."}, guidance_tracker:{name:"GUIDANCE", full:T("Guidance Reliability"), max:15, methodology:"Evaluates the consistency of quarterly guidance and consensus revisions. Penalises recent cuts."}, insider_flow: {name:"INSIDER", full:T("Insider Activity"), max:12, methodology:"SEC Form 4 + EODHD insider transactions. Net buying / selling weighted by management-signal vs stock-options-vesting noise."}, atlas: {name:"ATLAS", full:T("ATLAS · Intangibles & Moat"),max:20, methodology:"NLP + structured over 10-K. Detects IP, switching costs, network effects, scale, brand. Damodaran moat scoring."}, hermes: {name:"HERMES", full:"HERMES · NLP earnings calls",max:30, methodology:"Sentiment + management tone over earnings-call transcripts (Loughran-McDonald + in-house finetune). Compares Q&A vs prepared remarks."}, sector_agent: {name:"SECTOR", full:T("Sector timing"), max:10, methodology:"Sector cycle positioning + relative momentum. Adjusts for sector rotation flows."}, chronos: {name:"CHRONOS", full:T("CHRONOS · Cycle timing"), max:10, methodology:"Macro + sector cycle positioning (yield curve, PMI, sector rotation). Signals timing relative to the cycle."}, clio: {name:"CLIO", full:T("CLIO · Business model"), max:25, methodology:"LLM analysis of unit economics, customer concentration, scalability, competitive rivalry. CLIO Lite (Pro) vs CLIO Full (Trader+)."}, argos: {name:"ARGOS", full:T("ARGOS · Consensus & flows"), max:8, methodology:"Wisdom of crowds: sell-side consensus + ETF flows + short interest. Filters pure-momentum noise."} }; function mapBackendToTicker(d){ if (!d) return null; const sigParsed = parseSignal(d.action_signal && d.action_signal.signal); const confMap = {"ALTA":3,"HIGH":3,"MEDIA":2,"MEDIUM":2,"BAJA":1,"LOW":1}; const conf = ((d.action_signal && d.action_signal.confidence) || "").toUpperCase(); const confidence = confMap[conf] || 2; const confLabel = ({3:T("HIGH"),2:T("MEDIUM"),1:T("LOW")})[confidence] || T("MEDIUM"); const ticker = d.ticker || ""; const cur = currencyFor(ticker); // Sweet spot let sectorSweetSpot = d.sectorSweetSpot || null; if (!sectorSweetSpot && d.score_c1c5 !== undefined){ const sc = d.score_c1c5; sectorSweetSpot = { lo: Math.max(0, Math.floor(sc * 0.85)), hi: Math.min(100, Math.ceil(sc * 1.15)), label: ((d.grupo||"").replace(/_/g," ") + " · " + (d.lifecycle_stage||"").replace(/_/g," ")) || "Sector range", _derived: true }; } // Layers C1-C5 const layers = ["1","2","3","4","5"].map(k => { const c = (d.capas || {})[k] || {}; const meta = LAYER_META[k]; const max = c.score_max; const score = c.score; return { code: meta.code, name: meta.name, icon: meta.icon, pass: c.passed === true, score: (score !== null && score !== undefined) ? Math.round(score) : null, max: max || null, warn: (score !== null && score !== undefined && max && score < max * 0.5), detail: (c.summary || "").substring(0, 90), methodology: meta.methodology, drivers: (c.corrections || []).slice(0,5).map(x => { if (Array.isArray(x)) return x; if (typeof x === "object" && x !== null){ const k = x.descripcion || x.description || x.label || x.name || x.key || JSON.stringify(x).substring(0,40); const v = x.score !== undefined ? (x.score > 0 ? "+" : "") + x.score : (x.value || x.detail || ""); return [String(k).substring(0,60), String(v).substring(0,30)]; } return [String(x).substring(0,40), ""]; }) }; }); // Layers C6/C7 from ATLAS/HERMES agents (paper exposes 7 layers) if (d.agentes){ if (d.agentes.atlas){ layers.push({ code:"C6", name:"ATLAS · Intangibles", icon:"Home", pass:null, score: Math.round(d.agentes.atlas.score || 0), max:20, warn: (d.agentes.atlas.score || 0) < 10, detail: (d.agentes.atlas.summary || "").substring(0,90), methodology: AGENT_META.atlas.methodology, drivers: [] }); } if (d.agentes.hermes){ layers.push({ code:"C7", name:"HERMES · NLP", icon:"Wave", pass:null, score: Math.round(d.agentes.hermes.score || 0), max:30, warn: (d.agentes.hermes.score || 0) < 15, detail: (d.agentes.hermes.summary || "").substring(0,90), methodology: AGENT_META.hermes.methodology, drivers: [] }); } } // Agents const agentKeys = ["fcf_momentum","guidance_tracker","insider_flow","atlas","hermes","sector_agent","chronos","clio","argos"]; const agents = agentKeys.filter(k => d.agentes && d.agentes[k]).map(k => { const a = d.agentes[k]; const meta = AGENT_META[k]; const sc = a.score; const sign = sc > 0 ? "pos" : (sc < 0 ? "neg" : "amb"); return { name: meta.name, full: meta.full, v: (sc > 0 ? "+" : "") + ((sc !== null && sc !== undefined) ? Math.round(sc) : "—"), d: "/" + meta.max, sign: sign, tag: (a.summary || "").substring(0,40), methodology: meta.methodology, drivers: (a.signals || []).slice(0,5).map(s => { if (Array.isArray(s)) return s; if (typeof s === "object" && s !== null){ const k = s.descripcion || s.description || s.label || s.name || s.key || s.signal || JSON.stringify(s).substring(0,40); const v = s.score !== undefined ? (s.score > 0 ? "+" : "") + s.score : (s.value || s.detail || s.peso || ""); return [String(k).substring(0,60), String(v).substring(0,30)]; } return [String(s).substring(0,40), ""]; }) }; }); // DCF const dcf = d.dcf ? { current: d.dcf.current_price || (d.precio && d.precio.price) || 0, intrinsic: d.dcf.intrinsic_value_ps || 0, upside: d.dcf.margin_of_safety_pct || 0, method: "2-stage Mauboussin · WACC " + (d.dcf.wacc_pct ? d.dcf.wacc_pct.toFixed(1)+"%" : "—") + " · g " + (d.dcf.growth_rate_pct ? d.dcf.growth_rate_pct.toFixed(1)+"%" : "—"), range: { lo: d.dcf.intrinsic_value_bear || 0, hi: d.dcf.intrinsic_value_bull || 0 } } : null; // Stats const p = d.precio || {}; const stats = []; if (p.pe_current) stats.push({l:"P/E (TTM)", v: p.pe_current.toFixed(1) + "×"}); if (p.pe_fwd) stats.push({l:"P/E forward", v: p.pe_fwd.toFixed(1) + "×"}); if (p.pffo_ttm) stats.push({l:"P/FFO (TTM)", v: p.pffo_ttm.toFixed(1) + "×"}); if (p.rsi_14) stats.push({l:"RSI (14)", v: Math.round(p.rsi_14).toString()}); if (p.range_52w_pos_pct !== undefined && p.range_52w_pos_pct !== null) { // Backend already returns a percentage (0-100), not a fraction let pos52 = Number(p.range_52w_pos_pct); if (pos52 <= 1) pos52 = pos52 * 100; // safety: handle fraction format too stats.push({l:T("52w position"), v: pos52.toFixed(1) + "%"}); } if (p.epv_ratio) stats.push({l:"EPV ratio", v: p.epv_ratio.toFixed(2) + "×"}); if (d.gics_industry) stats.push({l:T("GICS industry"), v: d.gics_industry}); if (d.brand_equity){ const be = typeof d.brand_equity === "object" && d.brand_equity.score ? d.brand_equity.score.toFixed(1) : (typeof d.brand_equity === "number" ? d.brand_equity.toFixed(1) : "—"); stats.push({l:T("Brand equity"), v: be, g: true}); } stats.push({l:T("Engine"), v: "v6.1"}); if (d.analysis_ms) stats.push({l:T("Analysis"), v: Math.round(d.analysis_ms) + " ms"}); // Signal const signal = { verdict: sigParsed.verdict, pattern: sigParsed.pattern, confidence: confidence, confLabel: confLabel, desc: (d.action_signal && d.action_signal.rationale) || (d.action_signal && d.action_signal.headline) || (d.veredicto || ""), alpha3y: (d.action_signal && d.action_signal.alpha_3y) || "—", cagrAlpha: (d.action_signal && d.action_signal.cagr_alpha) || "—", winRate: (d.action_signal && d.action_signal.win_rate) || "—", pValue: (d.action_signal && d.action_signal.p_value) || "—" }; return { symbol: symbolFromTicker(ticker), market: marketCodeFromTicker(ticker), flag: flagForMarket(ticker), name: d.company_name || ticker, sector: (d.grupo || "").replace(/_/g," "), sub: d.gics_industry || "", lifecycle: (d.lifecycle_stage || "").replace(/_/g," "), lifecycleConfidence: d.lifecycle_conf ? Math.round(d.lifecycle_conf * 100) : null, price: (p.price !== undefined && p.price !== null) ? p.price : 0, change: p.change || 0, changePct: p.change_pct || 0, currency: cur, asOf: d.generated_at ? new Date(d.generated_at).toLocaleString("en-GB", {day:"numeric",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}) : "—", lastAnalysis: timeAgoStr(d.generated_at), score_display: Math.round(d.score_display || d.score || 0), score_c1c5: Math.round(d.score_c1c5 || 0), score_with_agents: Math.round(d.score || 0), sectorSweetSpot: sectorSweetSpot, signal: signal, layers: layers, agents: agents, dcf: dcf, stats: stats, news: d._news || [], alerts: d._alerts || [], // S12_MNEMO_UI_v1: MNEMO narrative narrative: d.narrative ? { available: d.narrative.data_available !== false, oneLiner: d.narrative.summary_one_liner || "", textEn: d.narrative.narrative_en || "", textEs: d.narrative.narrative_es || "", lifecycleStage: (d.narrative.lifecycle_focus_applied || "unknown").toUpperCase(), depth: d.narrative.depth || "lite", degraded: d.narrative.degraded === true, plan: (d.narrative.plan || "basic") } : null }; } /* ========================================================= APP · with real backend fetch + auth + login modal ========================================================= */ function App() { const [theme, setTheme] = useState(localStorage.getItem("vq_theme") || "dark"); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [watch, setWatch] = useState(false); const [tickerArg, setTickerArg] = useState(""); const [isDemo, setIsDemo] = useState(false); // === D2-C-React polling === const [retryAttempt, setRetryAttempt] = useState(0); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("vq_theme", theme); }, [theme]); useEffect(() => { const params = new URLSearchParams(window.location.search); const t = (params.get("t") || "").toUpperCase().trim(); const demo = params.get("demo") === "1"; setTickerArg(t); setIsDemo(demo); if (!t){ setLoading(false); setError({status: 400, message: "Missing ?t=TICKER in URL"}); return; } if (!demo && !vqApi.isLoggedIn()){ setLoading(false); setError({status: 401, message: T("Sign in required")}); setTimeout(() => { const dlg = document.getElementById("modal-login"); if (dlg && !dlg.open) dlg.showModal(); }, 100); return; } const forceRefresh = params.get("refresh") === "1"; let endpoint = demo ? "/demo/" + encodeURIComponent(t) : "/analyze/" + encodeURIComponent(t); if (forceRefresh && !demo) endpoint += "?refresh=1"; let cancelled = false; // === D2-C-React polling === // Reintenta si Cloudflare corta a 100s con 524 (motor lento). // Cada reintento aprovecha caches intermedias del motor. const REQ_TIMEOUT_MS = 95000; // <100s de Cloudflare const POLL_DELAY_MS = 60000; // S12_FIX_COST: 60s entre retries (era 30s) const MAX_RETRIES = 2; // S12_FIX_COST: 3 intentos max (era 7), reduce coste 4x async function fetchWithTimeout(path, ms){ const ctl = new AbortController(); const tid = setTimeout(() => ctl.abort(), ms); try { return await vqApi.get(path, {signal: ctl.signal}); } finally { clearTimeout(tid); } } async function loadMainWithRetry(){ let lastErr; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++){ if (cancelled) throw new Error('cancelled'); if (attempt > 0) setRetryAttempt(attempt); try { return await fetchWithTimeout(endpoint, REQ_TIMEOUT_MS); } catch(err){ lastErr = err; const status = err && err.status; const isRetriable = ( (err && (err.isAbort || err.isNetwork)) || status === 524 || status === 502 || status === 503 || status === 504 ); if (attempt >= MAX_RETRIES || !isRetriable) throw err; console.info('[loadAll] retry', attempt+1, 'in', POLL_DELAY_MS/1000, 's:', (err && err.message || '').slice(0, 80)); await new Promise(r => setTimeout(r, POLL_DELAY_MS)); } } throw lastErr; } async function loadAll(){ try { const main = await loadMainWithRetry(); if (cancelled) return; let news = [], alerts = []; if (!demo){ try { const newsRes = await vqApi.get("/news/" + encodeURIComponent(t)); news = newsRes.news || newsRes.items || []; } catch(e){ /* silent */ } try { const alertsRes = await vqApi.get("/ticker/" + encodeURIComponent(t) + "/alerts"); alerts = alertsRes.alerts || alertsRes.items || []; } catch(e){ /* silent */ } } main._news = news; main._alerts = alerts; if (!cancelled){ setData(main); setLoading(false); } if (!demo && vqApi.isLoggedIn()){ try { const w = await vqApi.get("/watchlist"); const items = w.watchlist || w.items || []; if (!cancelled) setWatch(items.some(it => (it.ticker || it) === t)); } catch(e){ /* silent */ } } } catch(err){ if (cancelled) return; setLoading(false); setError({status: err.status || 500, message: err.message || "Network error"}); } } loadAll(); return () => { cancelled = true; }; }, []); async function toggleWatch(){ if (!vqApi.isLoggedIn()){ const dlg = document.getElementById("modal-login"); if (dlg) dlg.showModal(); return; } try { if (watch){ await vqApi.del("/watchlist/" + encodeURIComponent(tickerArg)); setWatch(false); vqApi.toast(tickerArg + " " + T("removed from watchlist"), "info"); } else { // Backend expects 'ticker' as a query param, not JSON body await vqApi.post("/watchlist?ticker=" + encodeURIComponent(tickerArg), {}); setWatch(true); vqApi.toast(tickerArg + " " + T("added to watchlist"), "ok"); } } catch(err){ vqApi.toast(err.message || T("Watchlist error"), "err"); } } if (loading){ return ( <>