""" research.py — Research Note (beta): a first slice of "Auto Research / auto report". V1 scope (what this file does today): input ticker → pull fundamentals via yfinance .info + recent headlines → local LLM writes a structured English research note covering valuation, moat / supply-chain position, bull vs bear case, key risks. V2 (not built yet, see Automation tab description): multi-step agentic research (filings, peers, news crawl) and an automatic trigger that generates a report whenever a NEW ticker enters the pool. """ from __future__ import annotations import traceback _FIELDS = [ ("longName", "Name"), ("sector", "Sector"), ("industry", "Industry"), ("marketCap", "Market cap"), ("trailingPE", "P/E (ttm)"), ("forwardPE", "P/E (fwd)"), ("priceToSalesTrailing12Months", "P/S (ttm)"), ("priceToBook", "P/B"), ("enterpriseToEbitda", "EV/EBITDA"), ("profitMargins", "Net margin"), ("grossMargins", "Gross margin"), ("operatingMargins", "Operating margin"), ("returnOnEquity", "ROE"), ("revenueGrowth", "Revenue growth (yoy)"), ("earningsGrowth", "Earnings growth (yoy)"), ("freeCashflow", "Free cash flow"), ("totalCash", "Total cash"), ("totalDebt", "Total debt"), ("dividendYield", "Dividend yield"), ("beta", "Beta"), ("fiftyTwoWeekHigh", "52w high"), ("fiftyTwoWeekLow", "52w low"), ("currentPrice", "Price"), ("targetMeanPrice", "Analyst mean target"), ("recommendationKey", "Street view"), ] def _fmt(key: str, v): if v is None: return None try: if key in ("marketCap", "freeCashflow", "totalCash", "totalDebt"): v = float(v) return f"${v/1e9:,.1f}B" if abs(v) >= 1e9 else f"${v/1e6:,.0f}M" if key in ("profitMargins", "grossMargins", "operatingMargins", "returnOnEquity", "revenueGrowth", "earningsGrowth", "dividendYield"): return f"{float(v):.1%}" if isinstance(v, float): return f"{v:,.2f}" except (TypeError, ValueError): pass return str(v) def gather_facts(ticker: str) -> tuple: """Return (facts_markdown, facts_plain, error).""" try: import yfinance as yf tk = yf.Ticker(ticker) info = tk.info or {} except Exception as e: traceback.print_exc() return "", "", f"Could not fetch fundamentals for {ticker}: {e}" if not info or info.get("regularMarketPrice") is None and info.get("currentPrice") is None \ and not info.get("longName"): return "", "", f"No fundamentals returned for {ticker} — check the symbol." rows, plain = [], [] for key, label in _FIELDS: val = _fmt(key, info.get(key)) if val is None: continue rows.append(f"| {label} | {val} |") plain.append(f"{label}: {val}") summary = (info.get("longBusinessSummary") or "")[:900] heads = [] try: for x in (tk.news or [])[:6]: c = x.get("content", x) t = c.get("title") if t: heads.append(t) except Exception: pass md = f"**{info.get('longName', ticker)}** ({ticker})\n\n" md += "| Metric | Value |\n|---|---|\n" + "\n".join(rows) if heads: md += "\n\n**Recent headlines:** " + " · ".join(heads[:5]) plain_txt = "\n".join(plain) if summary: plain_txt += f"\nBusiness: {summary}" if heads: plain_txt += "\nRecent headlines: " + " | ".join(heads) return md, plain_txt, ""