""" Tab 11 — Earnings Estimates & Outlook Analyst consensus estimates, revenue/EPS forecasts, earnings history, price targets. """ import streamlit as st import pandas as pd from config.constants import MARKET_OPTIONS from utils.ticker import get_global_ticker from data.estimates import get_analyst_estimates, get_earnings_dates, format_estimate_table from data.fundamentals import get_sector_industry try: import plotly.graph_objects as go except ImportError: go = None def _render_price_targets(targets: dict): """Render analyst price target visualization.""" if not targets or not targets.get("mean"): return st.markdown("#### Analyst Price Targets") cur = targets.get("current") or 0 mean = targets.get("mean") or 0 high = targets.get("high") or 0 low = targets.get("low") or 0 median = targets.get("median") or 0 rec = targets.get("recommendation", "N/A") n = targets.get("num_analysts") or 0 # Recommendation badge color rec_colors = { "strongBuy": "#34D399", "buy": "#34D399", "overweight": "#34D399", "hold": "#FBBF24", "neutral": "#FBBF24", "sell": "#F87171", "underperform": "#F87171", "strongSell": "#F87171", } rec_color = rec_colors.get(rec, "#9CA3AF") upside = ((mean - cur) / cur * 100) if cur > 0 else 0 c1, c2, c3, c4 = st.columns(4) with c1: st.metric("Current Price", f"${cur:,.2f}" if cur else "N/A") with c2: st.metric("Target (Mean)", f"${mean:,.2f}" if mean else "N/A", delta=f"{upside:+.1f}%") with c3: st.metric("Target (Median)", f"${median:,.2f}" if median else "N/A") with c4: st.markdown( f'
' f'
Recommendation
' f'
{rec}
' f'
{n} analysts
', unsafe_allow_html=True, ) # Price target range bar if low and high and cur: st.markdown( f'
' f'
' f'Low: ${low:,.2f}' f'Mean: ${mean:,.2f}' f'High: ${high:,.2f}
' f'
' f'
', unsafe_allow_html=True, ) def _render_estimates_table(label: str, df): """Render an estimates DataFrame as a styled table.""" if df is None or (hasattr(df, 'empty') and df.empty): return st.markdown(f"#### {label}") display = format_estimate_table(df) if isinstance(df, pd.DataFrame) else df st.dataframe(display, use_container_width=True) def _render_earnings_history(dates_df: pd.DataFrame): """Render earnings history with surprise data.""" if dates_df is None or dates_df.empty: st.caption("No earnings history available.") return st.markdown("#### Earnings History & Surprises") display = dates_df.copy() for col in display.columns: if display[col].dtype in ('float64', 'float32'): display[col] = display[col].apply( lambda v: f"{v:.4f}" if pd.notna(v) and abs(v) < 10 else (f"{v:,.2f}" if pd.notna(v) else "N/A") ) st.dataframe(display, use_container_width=True) def _render_growth_estimates(ge): """Render growth estimates comparison table.""" if ge is None or (hasattr(ge, 'empty') and ge.empty): return st.markdown("#### Growth Estimates") st.dataframe(ge, use_container_width=True) def render_tab11(ticker): """Render the Estimates & Outlook tab.""" market = st.session_state.get("market") or MARKET_OPTIONS[0] quant_ticker = get_global_ticker(ticker, market) if ticker else "" st.subheader("Earnings Estimates & Outlook") if not ticker: st.info("Select a company from the sidebar to view estimates.") return si = get_sector_industry(quant_ticker) st.caption(f"**{ticker}** · {si.get('sector', 'N/A')} · {si.get('industry', 'N/A')}") with st.spinner("Fetching analyst estimates..."): estimates = get_analyst_estimates(quant_ticker) earnings_dates = get_earnings_dates(quant_ticker) if not estimates: st.warning("No analyst estimates available for this ticker.") return # ── Price Targets ── _render_price_targets(estimates.get("targets", {})) st.markdown("---") # ── Estimates Tables ── left, right = st.columns(2) with left: _render_estimates_table("Revenue Estimates", estimates.get("revenue_estimate")) _render_estimates_table("EPS Trend", estimates.get("eps_trend")) with right: _render_estimates_table("Earnings Estimates", estimates.get("earnings_estimate")) _render_growth_estimates(estimates.get("growth_estimates")) st.markdown("---") # ── Earnings History ── _render_earnings_history(earnings_dates)