Spaces:
Running
Running
| 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 ---- | |
| 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): | |
| ticker = st.text_input("Ticker:", "ASML", help="Insert the stock ticker.") | |
| # Radio selection for Annual vs Quarterly data | |
| data_period = st.radio("Select Data Period", ("Annual", "Quarterly")) | |
| if data_period == "Annual": | |
| period_api = "annual" | |
| period_count = st.number_input( | |
| "Years of historical data:", | |
| min_value=1, | |
| max_value=50, | |
| value=15, | |
| help="Choose how many years of historical data to retrieve." | |
| ) | |
| cutoff_date = datetime.now() - dateutil.relativedelta.relativedelta(years=period_count) | |
| xaxis_title = "Year" | |
| tickformat = "%Y" | |
| dtick = "M12" | |
| HIST_KEY = "historical_df_annual" | |
| FORECAST_KEY = "forecast_df_annual" | |
| else: | |
| period_api = "quarter" | |
| period_count = st.number_input( | |
| "Quarters of historical data:", | |
| min_value=1, | |
| max_value=200, | |
| value=20, | |
| help="Choose how many quarters of historical data to retrieve." | |
| ) | |
| cutoff_date = datetime.now() - dateutil.relativedelta.relativedelta(months=period_count * 3) | |
| xaxis_title = "Quarter" | |
| tickformat = "%Y-%m" | |
| dtick = "M3" | |
| HIST_KEY = "historical_df_quarter" | |
| FORECAST_KEY = "forecast_df_quarter" | |
| run_button = st.sidebar.button("Run Analysis") | |
| # Initialize session state if not present | |
| if HIST_KEY not in st.session_state: | |
| st.session_state[HIST_KEY] = pd.DataFrame() | |
| if FORECAST_KEY not in st.session_state: | |
| st.session_state[FORECAST_KEY] = pd.DataFrame() | |
| # ---- HELPER FUNCTION: VALUE FORMATTING ---- | |
| def format_value(x): | |
| 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}" | |
| def fetch_data(hist_url, forecast_url): | |
| hist_data = requests.get(hist_url, timeout=10).json() | |
| forecast_data = requests.get(forecast_url, timeout=10).json() | |
| return hist_data, forecast_data | |
| # ---- 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 over your selected period." | |
| ) | |
| if run_button: | |
| if not ticker.strip(): | |
| st.error("Please enter a valid ticker.") | |
| return | |
| hist_url = ( | |
| f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}" | |
| f"?period={period_api}&limit={period_count}&apikey={API_KEY}" | |
| ) | |
| forecast_url = ( | |
| f"https://financialmodelingprep.com/api/v3/analyst-estimates/{ticker}" | |
| f"?period={period_api}&apikey={API_KEY}" | |
| ) | |
| try: | |
| hist_data, forecast_data = fetch_data(hist_url, forecast_url) | |
| except Exception: | |
| st.error("Could not retrieve data at this time.") | |
| return | |
| st.session_state[HIST_KEY] = pd.DataFrame(hist_data) | |
| st.session_state[FORECAST_KEY] = pd.DataFrame(forecast_data) | |
| if st.session_state[HIST_KEY].empty and st.session_state[FORECAST_KEY].empty: | |
| st.info("Set your inputs in the sidebar, then click **Run Analysis**.") | |
| return | |
| historical_df = st.session_state[HIST_KEY] | |
| forecast_df = st.session_state[FORECAST_KEY] | |
| 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) | |
| 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] | |
| 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" | |
| } | |
| } | |
| } | |
| def create_plot(metric_name, hist_col, forecast_cols): | |
| fig = go.Figure() | |
| 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" | |
| )) | |
| 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}" | |
| )) | |
| if metric_name == "EPS": | |
| analyst_field = "numberAnalystsEstimatedEps" | |
| else: | |
| analyst_field = "numberAnalystEstimatedRevenue" | |
| 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_text = f"{ticker} - {metric_name} | Analysts: {analysts_count}" | |
| fig.update_layout( | |
| title=title_text, | |
| barmode="stack", | |
| template="plotly_dark", | |
| paper_bgcolor="#0e1117", | |
| plot_bgcolor="#0e1117", | |
| xaxis=dict( | |
| title=xaxis_title, | |
| tickangle=45, | |
| tickformat=tickformat, | |
| dtick=dtick, | |
| 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 | |
| for metric, mapping in metrics.items(): | |
| with st.container(border=True): | |
| st.subheader(metric) | |
| st.write( | |
| f"This chart shows {metric} over the selected time periods. " | |
| f"Bars represent historical data and lines represent forecast ranges. " | |
| "Hover over markers for details." | |
| ) | |
| fig = create_plot(metric, mapping["historical"], mapping["forecast"]) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with st.expander(f"View {metric} Data", expanded=False): | |
| hc = mapping["historical"] | |
| hist_disp = ( | |
| historical_df[["date", hc]].copy() | |
| if hc in historical_df.columns else pd.DataFrame() | |
| ) | |
| if not hist_disp.empty: | |
| hist_disp.rename(columns={hc: f"{metric}_Historical"}, inplace=True) | |
| forecast_disp = pd.DataFrame() | |
| if not forecast_df.empty: | |
| wanted_cols = ["date"] + list(mapping["forecast"].values()) | |
| existing_cols = [c for c in wanted_cols if c in forecast_df.columns] | |
| forecast_disp = forecast_df[existing_cols].copy() | |
| 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 | |
| ) | |
| 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: | |
| st.dataframe(merged_df.reset_index(drop=True)) | |
| if __name__ == "__main__": | |
| main() | |
| st.markdown( | |
| """ | |
| <style> | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| </style> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |