diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/valuation.py | 95 |
1 files changed, 83 insertions, 12 deletions
diff --git a/components/valuation.py b/components/valuation.py index d7a84d4..170ae1f 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -27,6 +27,29 @@ FINANCIAL_INDUSTRY_KEYWORDS = ( "reit", ) +INDUSTRY_PEER_MAP = { + "consumer electronics": ["AAPL", "SONY", "DELL", "HPQ", "LOGI"], + "software - infrastructure": ["MSFT", "ORCL", "CRM", "NOW", "SNOW"], + "semiconductors": ["NVDA", "AMD", "AVGO", "QCOM", "INTC"], + "internet content & information": ["GOOGL", "META", "PINS", "SNAP", "RDDT"], + "banks - diversified": ["JPM", "BAC", "WFC", "C", "GS"], + "credit services": ["V", "MA", "AXP", "DFS", "COF"], + "insurance - diversified": ["BRK-B", "AIG", "ALL", "TRV", "CB"], + "reit - industrial": ["PLD", "PSA", "EXR", "COLD", "REXR"], +} +SECTOR_PEER_MAP = { + "Technology": ["AAPL", "MSFT", "NVDA", "ORCL", "ADBE"], + "Communication Services": ["GOOGL", "META", "NFLX", "TMUS", "DIS"], + "Consumer Cyclical": ["AMZN", "TSLA", "HD", "MCD", "NKE"], + "Consumer Defensive": ["WMT", "COST", "PG", "KO", "PEP"], + "Financial Services": ["JPM", "BAC", "WFC", "GS", "MS"], + "Healthcare": ["LLY", "UNH", "JNJ", "MRK", "PFE"], + "Industrials": ["GE", "CAT", "RTX", "UPS", "UNP"], + "Energy": ["XOM", "CVX", "COP", "SLB", "EOG"], + "Utilities": ["NEE", "DUK", "SO", "AEP", "XEL"], + "Real Estate": ["PLD", "AMT", "EQIX", "O", "SPG"], +} + def _is_financial_company(info: dict) -> bool: sector = str(info.get("sector") or "").strip() @@ -36,6 +59,26 @@ def _is_financial_company(info: dict) -> bool: return any(keyword in industry for keyword in FINANCIAL_INDUSTRY_KEYWORDS) +def _suggest_peer_tickers(ticker: str, info: dict) -> list[str]: + industry = str(info.get("industry") or "").strip().lower() + sector = str(info.get("sector") or "").strip() + + candidates = [] + if industry in INDUSTRY_PEER_MAP: + candidates.extend(INDUSTRY_PEER_MAP[industry]) + if not candidates and sector in SECTOR_PEER_MAP: + candidates.extend(SECTOR_PEER_MAP[sector]) + + candidates = [c.upper() for c in candidates if c.upper() != ticker.upper()] + seen = set() + deduped = [] + for c in candidates: + if c not in seen: + deduped.append(c) + seen.add(c) + return deduped[:8] + + def render_valuation(ticker: str): tab_ratios, tab_dcf, tab_comps, tab_analyst, tab_earnings = st.tabs([ "Key Ratios", "DCF Model", "Comps", "Analyst Targets", "Earnings History" @@ -85,7 +128,7 @@ def _render_ratios(ticker: str): ("Forward P/E", fmt_ratio(info.get("forwardPE")) if info.get("forwardPE") is not None else "—"), ("P/S (TTM)", r("priceToSalesRatioTTM", "priceToSalesTrailing12Months")), ("P/B", r("priceToBookRatioTTM", "priceToBook")), - ("EV/EBITDA", r("enterpriseValueMultipleTTM", "enterpriseToEbitda")), + ("EV/EBITDA", r("enterpriseValueMultipleTTM", "enterpriseToEbitda") if ratios.get("enterpriseValueMultipleTTM") is not None else r("evToEBITDATTM", "enterpriseToEbitda")), ("EV/Revenue", r("evToSalesTTM", "enterpriseToRevenue")), ]), ("Profitability", [ @@ -97,12 +140,12 @@ def _render_ratios(ticker: str): ("ROIC", r("returnOnInvestedCapitalTTM", fmt=fmt_pct)), ]), ("Leverage & Liquidity", [ - ("Debt/Equity", r("debtEquityRatioTTM", "debtToEquity")), + ("Debt/Equity", r("debtToEquityRatioTTM", "debtToEquity")), ("Current Ratio", r("currentRatioTTM", "currentRatio")), ("Quick Ratio", r("quickRatioTTM", "quickRatio")), - ("Interest Coverage", r("interestCoverageTTM")), + ("Interest Coverage", r("interestCoverageRatioTTM")), ("Dividend Yield", r("dividendYieldTTM", "dividendYield", fmt_pct)), - ("Payout Ratio", r("payoutRatioTTM", "payoutRatio", fmt_pct)), + ("Payout Ratio", r("dividendPayoutRatioTTM", "payoutRatio", fmt_pct)), ]), ] @@ -305,18 +348,40 @@ def _render_dcf(ticker: str): # ── Comps Table ────────────────────────────────────────────────────────────── def _render_comps(ticker: str): - peers = get_peers(ticker) - if not peers: - st.info("No comparable companies found. Check your FMP API key.") - return + info = get_company_info(ticker) + auto_peers = get_peers(ticker) + suggested_peers = _suggest_peer_tickers(ticker, info) + + default_peer_string = ", ".join(auto_peers or suggested_peers) + manual_peer_string = st.text_input( + "Peer tickers", + value=default_peer_string, + help="Edit the comparable-company set manually. Comma-separated tickers.", + key=f"peer_input_{ticker.upper()}", + ) - all_tickers = [ticker.upper()] + [p for p in peers[:9] if p != ticker.upper()] + if auto_peers: + st.caption("Using FMP-discovered peers.") + elif suggested_peers: + st.caption("Using Prism fallback peers based on sector/industry. Edit them if you want a tighter comp set.") + else: + st.caption("No automatic peer set found. Enter peer tickers manually to build a comps table.") + + manual_peers = [p.strip().upper() for p in manual_peer_string.split(",") if p.strip()] + peer_list = [] + seen = {ticker.upper()} + for peer in manual_peers: + if peer not in seen: + peer_list.append(peer) + seen.add(peer) + + all_tickers = [ticker.upper()] + peer_list[:9] with st.spinner("Loading comps..."): ratios_list = get_ratios_for_tickers(all_tickers) if not ratios_list: - st.info("Could not load ratios for peer companies.") + st.info("Could not load ratios for the selected peer companies.") return display_cols = { @@ -325,13 +390,19 @@ def _render_comps(ticker: str): "priceToSalesRatioTTM": "P/S", "priceToBookRatioTTM": "P/B", "enterpriseValueMultipleTTM": "EV/EBITDA", + "evToEBITDATTM": "EV/EBITDA", "netProfitMarginTTM": "Net Margin", "returnOnEquityTTM": "ROE", - "debtEquityRatioTTM": "D/E", + "debtToEquityRatioTTM": "D/E", } df = pd.DataFrame(ratios_list) - available = [c for c in display_cols if c in df.columns] + if "enterpriseValueMultipleTTM" not in df.columns and "evToEBITDATTM" in df.columns: + df["enterpriseValueMultipleTTM"] = df["evToEBITDATTM"] + if "debtToEquityRatioTTM" not in df.columns and "debtEquityRatioTTM" in df.columns: + df["debtToEquityRatioTTM"] = df["debtEquityRatioTTM"] + + available = [c for c in ["symbol", "peRatioTTM", "priceToSalesRatioTTM", "priceToBookRatioTTM", "enterpriseValueMultipleTTM", "netProfitMarginTTM", "returnOnEquityTTM", "debtToEquityRatioTTM"] if c in df.columns] df = df[available].rename(columns=display_cols) pct_cols = {"Net Margin", "ROE"} |
