QuantumLearner commited on
Commit
219f3ee
·
verified ·
1 Parent(s): 5076d5d

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -0
app.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import pandas as pd
4
+ import plotly.graph_objects as go
5
+ from datetime import datetime
6
+ import dateutil.relativedelta
7
+ import os
8
+
9
+ # ---- PAGE CONFIG ----
10
+ # Makes the layout span the full width of the browser
11
+ st.set_page_config(layout="wide")
12
+
13
+ # ---- GLOBALS ----
14
+ API_KEY = os.getenv("FMP_API_KEY")
15
+
16
+ # ---- SIDEBAR INPUTS ----
17
+ st.sidebar.title("User Inputs")
18
+
19
+ with st.sidebar.expander("Configuration", expanded=True):
20
+ # Provide a tooltip for clarity
21
+ ticker = st.text_input("Ticker:", "ASML", help="Insert the stock ticker.")
22
+ # Let the user pick how many years of data to retrieve
23
+ years_back = st.number_input(
24
+ "Years of historical data:",
25
+ min_value=1,
26
+ max_value=50,
27
+ value=15,
28
+ help="Choose how many years of data to retrieve."
29
+ )
30
+
31
+ # A key button that triggers the data fetching and analysis
32
+ run_button = st.sidebar.button("Run Analysis")
33
+
34
+
35
+ # ---- HELPER FUNCTION: VALUE FORMATTING ----
36
+ def format_value(x):
37
+ # Formats large numeric values for readability
38
+ if abs(x) >= 1e9:
39
+ return f"{x/1e9:.1f}B"
40
+ elif abs(x) >= 1e6:
41
+ return f"{x/1e6:.1f}M"
42
+ elif abs(x) >= 1e3:
43
+ return f"{x/1e3:.1f}K"
44
+ else:
45
+ return f"{x:.1f}"
46
+
47
+
48
+ # ---- MAIN APP START ----
49
+ def main():
50
+ st.title("Analyst Estimates")
51
+ st.write("This tool fetches historical financial data and analyst forecasts. It helps you see past trends and future estimates.")
52
+
53
+ if not run_button:
54
+ st.info("Set your preferred inputs on the sidebar, then click **Run Analysis**.")
55
+ return
56
+
57
+ # Validate if ticker is provided
58
+ if not ticker.strip():
59
+ st.error("Please enter a valid ticker.")
60
+ return
61
+
62
+ # ---- FETCH AND PREPARE DATA ----
63
+ # Build the URLs using the global API_KEY
64
+ hist_url = (
65
+ f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}"
66
+ f"?period=annual&limit={years_back}&apikey={API_KEY}"
67
+ )
68
+ forecast_url = (
69
+ f"https://financialmodelingprep.com/api/v3/analyst-estimates/{ticker}"
70
+ f"?apikey={API_KEY}"
71
+ )
72
+
73
+ try:
74
+ # Attempt to request the data
75
+ hist_data = requests.get(hist_url, timeout=10).json()
76
+ forecast_data = requests.get(forecast_url, timeout=10).json()
77
+ except Exception:
78
+ st.error("Could not retrieve data at this time.")
79
+ return
80
+
81
+ # Convert raw JSON into DataFrames
82
+ historical_df = pd.DataFrame(hist_data)
83
+ forecast_df = pd.DataFrame(forecast_data)
84
+
85
+ # Basic check if data is not empty
86
+ if historical_df.empty and forecast_df.empty:
87
+ st.warning("No data found for the specified ticker.")
88
+ return
89
+
90
+ # Parse dates
91
+ if not historical_df.empty and "date" in historical_df.columns:
92
+ historical_df["date"] = pd.to_datetime(historical_df["date"])
93
+ historical_df.sort_values("date", inplace=True)
94
+ if not forecast_df.empty and "date" in forecast_df.columns:
95
+ forecast_df["date"] = pd.to_datetime(forecast_df["date"])
96
+ forecast_df.sort_values("date", inplace=True)
97
+
98
+ # Define a cutoff based on the number of years
99
+ cutoff_date = datetime.now() - dateutil.relativedelta.relativedelta(years=years_back)
100
+
101
+ # Filter the data within that range
102
+ if "date" in historical_df.columns:
103
+ historical_df = historical_df[historical_df["date"] >= cutoff_date]
104
+ if "date" in forecast_df.columns:
105
+ forecast_df = forecast_df[forecast_df["date"] >= cutoff_date]
106
+
107
+ # Dictionary that maps metric names to the corresponding columns
108
+ metrics = {
109
+ "Revenue": {
110
+ "historical": "revenue",
111
+ "forecast": {
112
+ "Low": "estimatedRevenueLow",
113
+ "Avg": "estimatedRevenueAvg",
114
+ "High": "estimatedRevenueHigh"
115
+ }
116
+ },
117
+ "EBITDA": {
118
+ "historical": "ebitda",
119
+ "forecast": {
120
+ "Low": "estimatedEbitdaLow",
121
+ "Avg": "estimatedEbitdaAvg",
122
+ "High": "estimatedEbitdaHigh"
123
+ }
124
+ },
125
+ "EBIT": {
126
+ "historical": "operatingIncome",
127
+ "forecast": {
128
+ "Low": "estimatedEbitLow",
129
+ "Avg": "estimatedEbitAvg",
130
+ "High": "estimatedEbitHigh"
131
+ }
132
+ },
133
+ "Net Income": {
134
+ "historical": "netIncome",
135
+ "forecast": {
136
+ "Low": "estimatedNetIncomeLow",
137
+ "Avg": "estimatedNetIncomeAvg",
138
+ "High": "estimatedNetIncomeHigh"
139
+ }
140
+ },
141
+ "SG&A Expense": {
142
+ "historical": "sellingGeneralAndAdministrativeExpenses",
143
+ "forecast": {
144
+ "Low": "estimatedSgaExpenseLow",
145
+ "Avg": "estimatedSgaExpenseAvg",
146
+ "High": "estimatedSgaExpenseHigh"
147
+ }
148
+ },
149
+ "EPS": {
150
+ "historical": "eps",
151
+ "forecast": {
152
+ "Low": "estimatedEpsLow",
153
+ "Avg": "estimatedEpsAvg",
154
+ "High": "estimatedEpsHigh"
155
+ }
156
+ }
157
+ }
158
+
159
+ # ---- PLOT CREATION FUNCTION ----
160
+ def create_plot(metric_name, hist_col, forecast_cols):
161
+ fig = go.Figure()
162
+
163
+ # Plot historical data as bars
164
+ if hist_col in historical_df.columns and not historical_df.empty:
165
+ bar_text = [format_value(val) for val in historical_df[hist_col]]
166
+ fig.add_trace(go.Bar(
167
+ x=historical_df["date"],
168
+ y=historical_df[hist_col],
169
+ text=bar_text,
170
+ textposition="auto",
171
+ name="Historical"
172
+ ))
173
+
174
+ # Plot forecast data as lines
175
+ if not forecast_df.empty:
176
+ for label, col in forecast_cols.items():
177
+ if col in forecast_df.columns:
178
+ fig.add_trace(go.Scatter(
179
+ x=forecast_df["date"],
180
+ y=forecast_df[col],
181
+ mode="lines+markers",
182
+ name=f"Forecast {label}"
183
+ ))
184
+
185
+ # Analyst count differs if metric is EPS
186
+ if metric_name == "EPS":
187
+ analyst_field = "numberAnalystsEstimatedEps"
188
+ else:
189
+ analyst_field = "numberAnalystEstimatedRevenue"
190
+
191
+ # Average number of analysts, if data present
192
+ if analyst_field in forecast_df.columns and not forecast_df.empty:
193
+ analysts_count = int(round(forecast_df[analyst_field].mean()))
194
+ else:
195
+ analysts_count = "N/A"
196
+
197
+ # Title
198
+ title_text = f"{ticker} - {metric_name} | Analysts: {analysts_count}"
199
+
200
+ # Layout updates
201
+ fig.update_layout(
202
+ title=title_text,
203
+ barmode="stack",
204
+ template="plotly_dark",
205
+ paper_bgcolor="black",
206
+ plot_bgcolor="black",
207
+ xaxis=dict(
208
+ title="Year",
209
+ tickangle=45,
210
+ tickformat="%Y",
211
+ dtick="M12",
212
+ showgrid=True,
213
+ gridcolor="rgba(255, 255, 255, 0.1)"
214
+ ),
215
+ yaxis=dict(
216
+ title=metric_name,
217
+ showgrid=True,
218
+ gridcolor="rgba(255, 255, 255, 0.1)"
219
+ ),
220
+ legend=dict(),
221
+ margin=dict(l=40, r=40, t=80, b=80)
222
+ )
223
+
224
+ return fig
225
+
226
+ # ---- DISPLAY RESULTS BY METRIC ----
227
+ for metric, mapping in metrics.items():
228
+ st.subheader(metric)
229
+ st.write(
230
+ f"This chart shows {metric} through the years. "
231
+ f"Bars represent historical numbers. Lines represent various forecast ranges. "
232
+ "Hover over the markers for details."
233
+ )
234
+
235
+ fig = create_plot(metric, mapping["historical"], mapping["forecast"])
236
+ st.plotly_chart(fig, use_container_width=True)
237
+
238
+ # Data expander at the end of each section
239
+ with st.expander(f"View {metric} Data", expanded=False):
240
+ # Show historical portion if available
241
+ relevant_cols = []
242
+ hc = mapping["historical"]
243
+ if hc in historical_df.columns:
244
+ relevant_cols.append(hc)
245
+ # Include forecast columns if present
246
+ for fc in mapping["forecast"].values():
247
+ if fc in forecast_df.columns:
248
+ relevant_cols.append(fc)
249
+
250
+ # Merge data for display
251
+ # We'll add a prefix to historical vs forecast columns to keep them separate
252
+ hist_disp = historical_df[["date", hc]].copy() if hc in historical_df.columns else pd.DataFrame()
253
+ hist_disp.rename(columns={hc: f"{metric}_Historical"}, inplace=True)
254
+
255
+ forecast_disp = forecast_df[["date"] + list(mapping["forecast"].values())].copy() if not forecast_df.empty else pd.DataFrame()
256
+ for fc_key, fc_val in mapping["forecast"].items():
257
+ if fc_val in forecast_disp.columns:
258
+ forecast_disp.rename(columns={fc_val: f"{metric}_Forecast_{fc_key}"}, inplace=True)
259
+
260
+ # Merge on date if both are non-empty
261
+ if not hist_disp.empty and not forecast_disp.empty:
262
+ merged_df = pd.merge(hist_disp, forecast_disp, on="date", how="outer")
263
+ merged_df.sort_values("date", inplace=True)
264
+ elif not hist_disp.empty:
265
+ merged_df = hist_disp
266
+ elif not forecast_disp.empty:
267
+ merged_df = forecast_disp
268
+ else:
269
+ merged_df = pd.DataFrame()
270
+
271
+ if merged_df.empty:
272
+ st.write("No data found for this metric.")
273
+ else:
274
+ # Show the data
275
+ st.dataframe(merged_df.reset_index(drop=True))
276
+
277
+ # ---- RUN ----
278
+ if __name__ == "__main__":
279
+ main()