import streamlit as st import requests import pandas as pd import plotly.graph_objects as go from datetime import datetime import dateutil.relativedelta import os # ---- PAGE CONFIG ---- # Makes the layout span the full width of the browser st.set_page_config(layout="wide") # ---- GLOBALS ---- API_KEY = os.getenv("FMP_API_KEY") # ---- SIDEBAR INPUTS ---- st.sidebar.title("User Inputs") with st.sidebar.expander("Configuration", expanded=True): # Provide a tooltip for clarity ticker = st.text_input("Ticker:", "ASML", help="Insert the stock ticker.") # Let the user pick how many years of data to retrieve years_back = st.number_input( "Years of historical data:", min_value=1, max_value=50, value=15, help="Choose how many years of data to retrieve." ) # A key button that triggers the data fetching and analysis run_button = st.sidebar.button("Run Analysis") # ---- HELPER FUNCTION: VALUE FORMATTING ---- def format_value(x): # Formats large numeric values for readability if abs(x) >= 1e9: return f"{x/1e9:.1f}B" elif abs(x) >= 1e6: return f"{x/1e6:.1f}M" elif abs(x) >= 1e3: return f"{x/1e3:.1f}K" else: return f"{x:.1f}" # ---- MAIN APP START ---- def main(): st.title("Analyst Forecasts & Estimates") st.write("This tool fetches historical financial data and analyst forecasts. It helps you see past trends and future estimates.") if not run_button: st.info("Set your preferred inputs on the sidebar, then click **Run Analysis**.") return # Validate if ticker is provided if not ticker.strip(): st.error("Please enter a valid ticker.") return # ---- FETCH AND PREPARE DATA ---- # Build the URLs using the global API_KEY hist_url = ( f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}" f"?period=annual&limit={years_back}&apikey={API_KEY}" ) forecast_url = ( f"https://financialmodelingprep.com/api/v3/analyst-estimates/{ticker}" f"?apikey={API_KEY}" ) try: # Attempt to request the data hist_data = requests.get(hist_url, timeout=10).json() forecast_data = requests.get(forecast_url, timeout=10).json() except Exception: st.error("Could not retrieve data at this time.") return # Convert raw JSON into DataFrames historical_df = pd.DataFrame(hist_data) forecast_df = pd.DataFrame(forecast_data) # Basic check if data is not empty if historical_df.empty and forecast_df.empty: st.warning("No data found for the specified ticker.") return # Parse dates if not historical_df.empty and "date" in historical_df.columns: historical_df["date"] = pd.to_datetime(historical_df["date"]) historical_df.sort_values("date", inplace=True) if not forecast_df.empty and "date" in forecast_df.columns: forecast_df["date"] = pd.to_datetime(forecast_df["date"]) forecast_df.sort_values("date", inplace=True) # Define a cutoff based on the number of years cutoff_date = datetime.now() - dateutil.relativedelta.relativedelta(years=years_back) # Filter the data within that range if "date" in historical_df.columns: historical_df = historical_df[historical_df["date"] >= cutoff_date] if "date" in forecast_df.columns: forecast_df = forecast_df[forecast_df["date"] >= cutoff_date] # Dictionary that maps metric names to the corresponding columns metrics = { "Revenue": { "historical": "revenue", "forecast": { "Low": "estimatedRevenueLow", "Avg": "estimatedRevenueAvg", "High": "estimatedRevenueHigh" } }, "EBITDA": { "historical": "ebitda", "forecast": { "Low": "estimatedEbitdaLow", "Avg": "estimatedEbitdaAvg", "High": "estimatedEbitdaHigh" } }, "EBIT": { "historical": "operatingIncome", "forecast": { "Low": "estimatedEbitLow", "Avg": "estimatedEbitAvg", "High": "estimatedEbitHigh" } }, "Net Income": { "historical": "netIncome", "forecast": { "Low": "estimatedNetIncomeLow", "Avg": "estimatedNetIncomeAvg", "High": "estimatedNetIncomeHigh" } }, "SG&A Expense": { "historical": "sellingGeneralAndAdministrativeExpenses", "forecast": { "Low": "estimatedSgaExpenseLow", "Avg": "estimatedSgaExpenseAvg", "High": "estimatedSgaExpenseHigh" } }, "EPS": { "historical": "eps", "forecast": { "Low": "estimatedEpsLow", "Avg": "estimatedEpsAvg", "High": "estimatedEpsHigh" } } } # ---- PLOT CREATION FUNCTION ---- def create_plot(metric_name, hist_col, forecast_cols): fig = go.Figure() # Plot historical data as bars if hist_col in historical_df.columns and not historical_df.empty: bar_text = [format_value(val) for val in historical_df[hist_col]] fig.add_trace(go.Bar( x=historical_df["date"], y=historical_df[hist_col], text=bar_text, textposition="auto", name="Historical" )) # Plot forecast data as lines if not forecast_df.empty: for label, col in forecast_cols.items(): if col in forecast_df.columns: fig.add_trace(go.Scatter( x=forecast_df["date"], y=forecast_df[col], mode="lines+markers", name=f"Forecast {label}" )) # Analyst count differs if metric is EPS if metric_name == "EPS": analyst_field = "numberAnalystsEstimatedEps" else: analyst_field = "numberAnalystEstimatedRevenue" # Average number of analysts, if data present if analyst_field in forecast_df.columns and not forecast_df.empty: analysts_count = int(round(forecast_df[analyst_field].mean())) else: analysts_count = "N/A" # Title title_text = f"{ticker} - {metric_name} | Analysts: {analysts_count}" # Layout updates fig.update_layout( title=title_text, barmode="stack", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( title="Year", tickangle=45, tickformat="%Y", dtick="M12", showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( title=metric_name, showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), legend=dict(), margin=dict(l=40, r=40, t=80, b=80) ) return fig # ---- DISPLAY RESULTS BY METRIC ---- for metric, mapping in metrics.items(): st.subheader(metric) st.write( f"This chart shows {metric} through the years. " f"Bars represent historical numbers. Lines represent various forecast ranges. " "Hover over the markers for details." ) fig = create_plot(metric, mapping["historical"], mapping["forecast"]) st.plotly_chart(fig, use_container_width=True) # Data expander at the end of each section with st.expander(f"View {metric} Data", expanded=False): # Show historical portion if available relevant_cols = [] hc = mapping["historical"] if hc in historical_df.columns: relevant_cols.append(hc) # Include forecast columns if present for fc in mapping["forecast"].values(): if fc in forecast_df.columns: relevant_cols.append(fc) # Merge data for display # We'll add a prefix to historical vs forecast columns to keep them separate hist_disp = historical_df[["date", hc]].copy() if hc in historical_df.columns else pd.DataFrame() hist_disp.rename(columns={hc: f"{metric}_Historical"}, inplace=True) forecast_disp = forecast_df[["date"] + list(mapping["forecast"].values())].copy() if not forecast_df.empty else pd.DataFrame() for fc_key, fc_val in mapping["forecast"].items(): if fc_val in forecast_disp.columns: forecast_disp.rename(columns={fc_val: f"{metric}_Forecast_{fc_key}"}, inplace=True) # Merge on date if both are non-empty if not hist_disp.empty and not forecast_disp.empty: merged_df = pd.merge(hist_disp, forecast_disp, on="date", how="outer") merged_df.sort_values("date", inplace=True) elif not hist_disp.empty: merged_df = hist_disp elif not forecast_disp.empty: merged_df = forecast_disp else: merged_df = pd.DataFrame() if merged_df.empty: st.write("No data found for this metric.") else: # Show the data st.dataframe(merged_df.reset_index(drop=True)) # ---- RUN ---- if __name__ == "__main__": main() # Hide default Streamlit style st.markdown( """ """, unsafe_allow_html=True )