"""Valuation panel — key ratios, DCF model, comparable companies.""" import pandas as pd import plotly.graph_objects as go import streamlit as st from services.data_service import get_company_info, get_free_cash_flow_series from services.fmp_service import get_key_ratios, get_peers, get_ratios_for_tickers from services.valuation_service import run_dcf from utils.formatters import fmt_ratio, fmt_pct, fmt_large, fmt_currency def render_valuation(ticker: str): tab_ratios, tab_dcf, tab_comps = st.tabs(["Key Ratios", "DCF Model", "Comps"]) with tab_ratios: _render_ratios(ticker) with tab_dcf: _render_dcf(ticker) with tab_comps: _render_comps(ticker) # ── Key Ratios ─────────────────────────────────────────────────────────────── def _render_ratios(ticker: str): ratios = get_key_ratios(ticker) info = get_company_info(ticker) if not ratios and not info: st.info("Ratio data unavailable. Check your FMP API key.") return # Prefer FMP ratios, fall back to yfinance info def r(fmp_key, yf_key=None, fmt=fmt_ratio): val = ratios.get(fmp_key) if ratios else None if val is None and yf_key and info: val = info.get(yf_key) return fmt(val) if val is not None else "—" rows = [ ("Valuation", [ ("P/E (TTM)", r("peRatioTTM", "trailingPE")), ("Forward P/E", r("priceEarningsRatioTTM", "forwardPE")), ("P/S (TTM)", r("priceToSalesRatioTTM", "priceToSalesTrailing12Months")), ("P/B", r("priceToBookRatioTTM", "priceToBook")), ("EV/EBITDA", r("enterpriseValueMultipleTTM", "enterpriseToEbitda")), ("EV/Revenue", r("evToSalesTTM", "enterpriseToRevenue")), ]), ("Profitability", [ ("Gross Margin", r("grossProfitMarginTTM", "grossMargins", fmt_pct)), ("Operating Margin", r("operatingProfitMarginTTM", "operatingMargins", fmt_pct)), ("Net Margin", r("netProfitMarginTTM", "profitMargins", fmt_pct)), ("ROE", r("returnOnEquityTTM", "returnOnEquity", fmt_pct)), ("ROA", r("returnOnAssetsTTM", "returnOnAssets", fmt_pct)), ("ROIC", r("returnOnInvestedCapitalTTM", fmt=fmt_pct)), ]), ("Leverage & Liquidity", [ ("Debt/Equity", r("debtEquityRatioTTM", "debtToEquity")), ("Current Ratio", r("currentRatioTTM", "currentRatio")), ("Quick Ratio", r("quickRatioTTM", "quickRatio")), ("Interest Coverage", r("interestCoverageTTM")), ("Dividend Yield", r("dividendYieldTTM", "dividendYield", fmt_pct)), ("Payout Ratio", r("payoutRatioTTM", "payoutRatio", fmt_pct)), ]), ] for section_name, metrics in rows: st.markdown(f"**{section_name}**") cols = st.columns(6) for col, (label, val) in zip(cols, metrics): col.metric(label, val) st.write("") # ── DCF Model ──────────────────────────────────────────────────────────────── def _render_dcf(ticker: str): info = get_company_info(ticker) shares = info.get("sharesOutstanding") or info.get("floatShares") current_price = info.get("currentPrice") or info.get("regularMarketPrice") if not shares: st.info("Shares outstanding not available — DCF cannot be computed.") return fcf_series = get_free_cash_flow_series(ticker) if fcf_series.empty: st.info("Free cash flow data unavailable.") return st.markdown("**Assumptions**") col1, col2, col3 = st.columns(3) with col1: wacc = st.slider("WACC (%)", min_value=5.0, max_value=20.0, value=10.0, step=0.5) / 100 with col2: terminal_growth = st.slider("Terminal Growth (%)", min_value=0.5, max_value=5.0, value=2.5, step=0.5) / 100 with col3: projection_years = st.slider("Projection Years", min_value=3, max_value=10, value=5, step=1) result = run_dcf( fcf_series=fcf_series, shares_outstanding=shares, wacc=wacc, terminal_growth=terminal_growth, projection_years=projection_years, ) if not result: st.warning("Insufficient data to run DCF model.") return iv = result["intrinsic_value_per_share"] # ── Summary metrics ────────────────────────────────────────────────────── m1, m2, m3, m4 = st.columns(4) m1.metric("Intrinsic Value / Share", fmt_currency(iv)) if current_price: upside = (iv - current_price) / current_price m2.metric("Current Price", fmt_currency(current_price)) m3.metric("Upside / Downside", f"{upside * 100:+.1f}%", delta=f"{upside * 100:+.1f}%") m4.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%") st.write("") # ── Waterfall chart ─────────────────────────────────────────────────────── years = [f"Year {y}" for y in result["years"]] discounted = result["discounted_fcfs"] terminal_pv = result["terminal_value_pv"] bar_labels = years + ["Terminal Value"] bar_values = discounted + [terminal_pv] bar_colors = ["#4F8EF7"] * len(years) + ["#F7A24F"] fig = go.Figure( go.Bar( x=bar_labels, y=[v / 1e9 for v in bar_values], marker_color=bar_colors, text=[f"${v / 1e9:.2f}B" for v in bar_values], textposition="outside", ) ) fig.update_layout( title="PV of Projected FCFs + Terminal Value (Billions)", yaxis_title="USD (Billions)", plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)", margin=dict(l=0, r=0, t=40, b=0), height=360, ) st.plotly_chart(fig, use_container_width=True) # ── 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 # Include the subject ticker all_tickers = [ticker.upper()] + [p for p in peers[:9] if p != ticker.upper()] 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.") return display_cols = { "symbol": "Ticker", "peRatioTTM": "P/E", "priceToSalesRatioTTM": "P/S", "priceToBookRatioTTM": "P/B", "enterpriseValueMultipleTTM": "EV/EBITDA", "netProfitMarginTTM": "Net Margin", "returnOnEquityTTM": "ROE", "debtEquityRatioTTM": "D/E", } df = pd.DataFrame(ratios_list) available = [c for c in display_cols if c in df.columns] df = df[available].rename(columns=display_cols) # Format numeric columns pct_cols = {"Net Margin", "ROE"} for col in df.columns: if col == "Ticker": continue if col in pct_cols: df[col] = df[col].apply(lambda v: fmt_pct(v) if v is not None else "—") else: df[col] = df[col].apply(lambda v: fmt_ratio(v) if v is not None else "—") # Highlight subject ticker row def highlight_subject(row): if row["Ticker"] == ticker.upper(): return ["background-color: rgba(79,142,247,0.15)"] * len(row) return [""] * len(row) st.dataframe( df.style.apply(highlight_subject, axis=1), use_container_width=True, hide_index=True, )