import pandas as pd import streamlit as st from config.constants import MARKET_OPTIONS, DAMODARAN_ERP_PCT, DAMODARAN_RF_PCT from utils.ticker import get_global_ticker from utils.formatting import _format_shares_display from utils.dcf import excel_style_dcf, _damodaran_wacc_for_sector from utils.charts import _apply_dark_theme from utils.ui_helpers import _render_analyst_consensus, _render_sensitivity_table from data.fundamentals import get_sector_industry, get_5yr_financial_trend, get_dcf_inputs from data.valuation import get_analyst_consensus, get_dcf_smart_defaults, get_fcff_fcfe_valuation try: import plotly.express as px except ImportError: px = None try: import yfinance as yf except ImportError: yf = None def render_tab2(ticker): market_t2 = st.session_state.get("market") or MARKET_OPTIONS[0] quant_ticker_t2 = get_global_ticker(ticker, market_t2) if ticker else "" st.subheader("5-Year Financial Trend & DCF Valuation") if ticker: si_t2 = get_sector_industry(quant_ticker_t2) sector_t2 = (si_t2.get("sector") or "").lower() is_financial = "financial" in sector_t2 or "bank" in sector_t2 or "insurance" in sector_t2 else: is_financial = False df_trend = get_5yr_financial_trend(quant_ticker_t2) if quant_ticker_t2 else pd.DataFrame() if not df_trend.empty and len(df_trend) >= 1: st.markdown("#### Key metrics (YoY % change)") latest = df_trend.iloc[0] prev = df_trend.iloc[1] if len(df_trend) >= 2 else None def _yoy_pct(cur, prev_val): if prev_val is None or cur is None or prev_val == 0: return None return (cur - prev_val) / abs(prev_val) * 100 rev_yoy = _yoy_pct(latest.get("Revenue"), prev.get("Revenue") if prev is not None else None) ni_yoy = _yoy_pct(latest.get("Net Income"), prev.get("Net Income") if prev is not None else None) om_prev = prev.get("Operating Margin %") if prev is not None else None om_cur = latest.get("Operating Margin %") om_yoy = (om_cur - om_prev) if (om_cur is not None and om_prev is not None) else None fcf_yoy = _yoy_pct(latest.get("FCF"), prev.get("FCF") if prev is not None else None) m1, m2, m3, m4 = st.columns(4) rev_val = latest.get("Revenue") m1.metric("Revenue (latest yr)", f"${rev_val/1e9:.2f}B" if rev_val and rev_val >= 1e9 else (f"${rev_val/1e6:.0f}M" if rev_val else "\u2014"), f"{rev_yoy:+.1f}% YoY" if rev_yoy is not None else None) ni_val = latest.get("Net Income") m2.metric("Net Income", f"${ni_val/1e9:.2f}B" if ni_val and abs(ni_val) >= 1e9 else (f"${ni_val/1e6:.0f}M" if ni_val is not None else "\u2014"), f"{ni_yoy:+.1f}% YoY" if ni_yoy is not None else None) om_val = latest.get("Operating Margin %") m3.metric("Operating Margin %", f"{om_val:.1f}%" if om_val is not None else "\u2014", f"{om_yoy:+.1f}pp YoY" if om_yoy is not None else None) fcf_val = latest.get("FCF") m4.metric("FCF", f"${fcf_val/1e9:.2f}B" if fcf_val and abs(fcf_val) >= 1e9 else (f"${fcf_val/1e6:.0f}M" if fcf_val is not None else "\u2014"), f"{fcf_yoy:+.1f}% YoY" if fcf_yoy is not None else None) st.caption("FCF = Operating Cash Flow \u2212 Capital Expenditure." + (" For Financials, FCF/EBITDA are less relevant; see ROE/ROA in Tab 1 sector-specific metrics." if is_financial else "")) if len(df_trend) >= 2 and px is not None: st.markdown("#### 5-year trend: Revenue & FCF") df_plot = df_trend.copy() df_plot["Revenue_M"] = (df_plot["Revenue"] / 1e6).round(1) df_plot["FCF_M"] = (df_plot["FCF"] / 1e6).round(1) fig = px.line(df_plot, x="Year", y=["Revenue_M", "FCF_M"], title="Revenue & Free Cash Flow ($M)") fig.update_layout(yaxis_title="$M", legend_title="", hovermode="x unified") fig.update_traces(line=dict(width=2)) _apply_dark_theme(fig) st.plotly_chart(fig, use_container_width=True) elif ticker: st.caption("5-year trend not available for this ticker. DCF section below uses latest FCF from yfinance.") st.markdown("---") st.markdown("#### DCF valuation (Excel-style): inputs & 3-scenario output") dcf_inputs = get_dcf_inputs(quant_ticker_t2) if quant_ticker_t2 else {"fcf": None, "total_debt": 0.0, "cash": 0.0, "shares": None} fcf_fetched = dcf_inputs.get("fcf") total_debt = float(dcf_inputs.get("total_debt") or 0.0) cash = float(dcf_inputs.get("cash") or 0.0) shares_fetched = dcf_inputs.get("shares") # Base FCF if fcf_fetched is None or fcf_fetched <= 0: fcf = st.number_input("Base FCF (manual \u2014 only if yfinance missing)", value=0.0, min_value=-1e12, step=1e8, format="%.0f", key="dcf_fcf_manual") else: fcf = float(fcf_fetched) st.caption(f"Base FCF (OCF \u2212 CapEx): **${fcf/1e9:.2f}B**" if abs(fcf) >= 1e9 else f"Base FCF (OCF \u2212 CapEx): **${fcf/1e6:.0f}M**") # Shares: auto-fetched (fast_info -> info -> balance); manual only as last resort if shares_fetched is not None and shares_fetched > 0: shares = float(shares_fetched) st.caption(f"Shares Outstanding: **{_format_shares_display(shares)}** (real-time, auto-fetched)") else: shares = st.number_input("Shares Outstanding (manual \u2014 only if all API sources failed)", value=1e9, min_value=1.0, step=1e7, format="%.0f", key="dcf_shares_manual") # Total Debt & Cash: manual only when both API sources completely failed if total_debt == 0 and cash == 0: c1, c2 = st.columns(2) with c1: total_debt = st.number_input("Total Debt (manual \u2014 only if all sources failed)", value=0.0, min_value=0.0, step=1e8, format="%.0f", key="dcf_debt_manual") with c2: cash = st.number_input("Cash & Equivalents (manual \u2014 only if all sources failed)", value=0.0, min_value=0.0, step=1e8, format="%.0f", key="dcf_cash_manual") else: st.caption(f"Total Debt: **${total_debt/1e9:.2f}B**" if total_debt >= 1e9 else f"Total Debt: **${total_debt/1e6:.0f}M**" if total_debt >= 1e6 else f"Total Debt: **${total_debt:,.0f}**") st.caption(f"Cash & Equivalents: **${cash/1e9:.2f}B**" if cash >= 1e9 else f"Cash & Equivalents: **${cash/1e6:.0f}M**" if cash >= 1e6 else f"Cash & Equivalents: **${cash:,.0f}**") dcf_defaults = get_dcf_smart_defaults(quant_ticker_t2) if quant_ticker_t2 else {"wacc_pct": 10.0, "term_growth_pct": 2.5, "fcf_growth_pct": 8.0} st.markdown("**Assumptions (sliders)**") st.caption("\U0001f4a1 Slider defaults are auto-generated based on the company's Beta (CAPM) and revenue growth estimates.") col1, col2, col3 = st.columns(3) with col1: wacc = st.slider("WACC (Discount Rate) %", 4.0, 20.0, float(dcf_defaults["wacc_pct"]), 0.5, key="dcf_wacc") / 100.0 with col2: term_growth = st.slider("Terminal Growth Rate %", -2.0, 6.0, float(dcf_defaults["term_growth_pct"]), 0.25, key="dcf_term") / 100.0 with col3: base_growth = st.slider("Projected FCF Growth (Stage 1, Y1\u20135) %", -10.0, 30.0, float(dcf_defaults["fcf_growth_pct"]), 0.5, key="dcf_fcf_growth") / 100.0 bull_growth = base_growth + 0.02 bear_growth = base_growth - 0.02 with st.expander("Reference: Analyst & Macro Assumptions", expanded=False): left_col, right_col = st.columns(2) with left_col: st.markdown("**Analyst consensus (yfinance)**") analyst = get_analyst_consensus(quant_ticker_t2) if quant_ticker_t2 else {} _tmp = analyst.get('targetMeanPrice') st.markdown(f"- **Target mean price:** ${_tmp:,.2f}" if _tmp else "- **Target mean price:** N/A") st.markdown(f"- **Recommendation:** {analyst.get('recommendationKey', 'N/A')}") st.markdown(f"- **Revenue growth est.:** {analyst.get('revenueGrowth', 'N/A')}") st.markdown(f"- **Earnings growth est.:** {analyst.get('earningsGrowth', 'N/A')}") with right_col: st.markdown("**Aswath Damodaran \u2014 macro baseline**") sector_name = get_sector_industry(ticker).get("sector", "N/A") if ticker else "N/A" damodaran_wacc = _damodaran_wacc_for_sector(sector_name) if ticker else 8.0 st.markdown(f"- **Sector WACC (ref.):** {damodaran_wacc:.1f}% (closest: {sector_name})") st.markdown(f"- **US equity risk premium (ERP):** {DAMODARAN_ERP_PCT}%") st.markdown(f"- **10Y risk-free rate:** {DAMODARAN_RF_PCT}%") st.markdown("[Data & methodology (Damodaran)](https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/wacc.htm) so users can verify.") res_base = excel_style_dcf(fcf, wacc, term_growth, base_growth, total_debt, cash, shares) res_bull = excel_style_dcf(fcf, wacc, term_growth, bull_growth, total_debt, cash, shares) res_bear = excel_style_dcf(fcf, wacc, term_growth, bear_growth, total_debt, cash, shares) price_base = res_base.get("value_per_share") or 0.0 price_bull = res_bull.get("value_per_share") or 0.0 price_bear = res_bear.get("value_per_share") or 0.0 current_price = None if quant_ticker_t2 and yf: try: info = yf.Ticker(quant_ticker_t2.upper()).info or {} current_price = info.get("currentPrice") or info.get("regularMarketPrice") or info.get("previousClose") except Exception: pass st.markdown("**Intrinsic value vs current price**") if current_price is not None and current_price > 0: st.metric("Current price", f"${current_price:.2f}", None) st.metric("Base case intrinsic value per share", f"${price_base:.2f}" if price_base else "N/A", f"vs current: {(price_base - current_price):.2f}" if (current_price and price_base) else None) c1, c2, c3 = st.columns(3) c1.metric("Bull (+2% FCF growth)", f"${price_bull:.2f}" if price_bull else "N/A", f"vs Base: +{(price_bull - price_base):.2f}" if (price_bull and price_base) else None) c2.metric("Base", f"${price_base:.2f}" if price_base else "N/A", "\u2014") c3.metric("Bear (\u22122% FCF growth)", f"${price_bear:.2f}" if price_bear else "N/A", f"vs Base: {(price_bear - price_base):.2f}" if (price_bear and price_base) else None) df_dcf = pd.DataFrame({ "Scenario": ["Bull", "Base", "Bear"], "FCF Growth %": [f"{bull_growth*100:.1f}", f"{base_growth*100:.1f}", f"{bear_growth*100:.1f}"], "Intrinsic Value ($)": [round(price_bull, 2) if price_bull else "N/A", round(price_base, 2) if price_base else "N/A", round(price_bear, 2) if price_bear else "N/A"], }) st.dataframe(df_dcf, use_container_width=True, hide_index=True) # --- Probability-Weighted Expected Return --- st.markdown("**Probability-Weighted Expected Return**") bull_prob, base_prob, bear_prob = 0.25, 0.55, 0.20 pw_value = bull_prob * price_bull + base_prob * price_base + bear_prob * price_bear if current_price and current_price > 0 and pw_value > 0: pw_return = (pw_value - current_price) / current_price * 100 st.markdown(f"""