QuantumLearner commited on
Commit
da8f8a9
·
verified ·
1 Parent(s): 3566ec7

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +410 -0
app.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ from plotly.subplots import make_subplots
6
+ import requests
7
+ import os
8
+
9
+ # -------------------------------------------------------
10
+ # GLOBAL CONFIG
11
+ # -------------------------------------------------------
12
+ API_KEY = os.getenv("FMP_API_KEY")
13
+ st.set_page_config(page_title="Financial Statements", layout="wide")
14
+
15
+ # Initialize session state for caching
16
+ if 'data_cache' not in st.session_state:
17
+ st.session_state.data_cache = {}
18
+
19
+ # -------------------------------------------------------
20
+ # CACHED FETCH FUNCTIONS
21
+ # -------------------------------------------------------
22
+ @st.cache_data
23
+ def fetch_income_statement(symbol: str, period: str, api_key: str) -> pd.DataFrame:
24
+ url = f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}?period={period}&apikey={api_key}"
25
+ r = requests.get(url)
26
+ r.raise_for_status()
27
+ data = r.json() if r.status_code == 200 else []
28
+ df = pd.DataFrame(data)
29
+ if not df.empty and "date" in df.columns:
30
+ df["date"] = pd.to_datetime(df["date"], errors="coerce")
31
+ df.sort_values("date", inplace=True)
32
+ return df
33
+
34
+ @st.cache_data
35
+ def fetch_balance_sheet(symbol: str, period: str, api_key: str) -> pd.DataFrame:
36
+ url = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}?period={period}&apikey={api_key}"
37
+ r = requests.get(url)
38
+ r.raise_for_status()
39
+ data = r.json() if r.status_code == 200 else []
40
+ df = pd.DataFrame(data)
41
+ if not df.empty and "date" in df.columns:
42
+ df["date"] = pd.to_datetime(df["date"], errors="coerce")
43
+ df.sort_values("date", inplace=True)
44
+ return df
45
+
46
+ @st.cache_data
47
+ def fetch_cash_flow(symbol: str, period: str, api_key: str) -> pd.DataFrame:
48
+ url = f"https://financialmodelingprep.com/api/v3/cash-flow-statement/{symbol}?period={period}&apikey={api_key}"
49
+ r = requests.get(url)
50
+ r.raise_for_status()
51
+ data = r.json() if r.status_code == 200 else []
52
+ df = pd.DataFrame(data)
53
+ if not df.empty and "date" in df.columns:
54
+ df["date"] = pd.to_datetime(df["date"], errors="coerce")
55
+ df.sort_values("date", inplace=True)
56
+ return df
57
+
58
+ # -------------------------------------------------------
59
+ # HELPER: CREATE DUAL-AXIS SUBPLOT
60
+ # -------------------------------------------------------
61
+ def create_dual_axis_figure(df: pd.DataFrame, vars_list: list[str], title: str, period: str) -> go.Figure:
62
+ shift_val = 1 if period == "annual" else 4
63
+ df_local = df.copy()
64
+
65
+ for var in vars_list:
66
+ if var in df_local.columns:
67
+ df_local[var + "_yoy"] = (
68
+ (df_local[var] - df_local[var].shift(shift_val))
69
+ / df_local[var].shift(shift_val)
70
+ ) * 100
71
+ else:
72
+ df_local[var + "_yoy"] = None
73
+
74
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
75
+ colors = px.colors.qualitative.Plotly
76
+
77
+ for idx, var in enumerate(vars_list):
78
+ color_idx = idx % len(colors)
79
+ base_color = colors[color_idx]
80
+ fig.add_trace(
81
+ go.Scatter(
82
+ x=df_local["date"],
83
+ y=df_local[var],
84
+ name=var,
85
+ mode="lines+markers",
86
+ line=dict(width=2, color=base_color),
87
+ hovertemplate=(f"<b>{var}</b><br>Date: %{{x}}<br>Value: %{{y:.2f}}<extra></extra>"),
88
+ ),
89
+ secondary_y=False
90
+ )
91
+ yoy_col = var + "_yoy"
92
+ fig.add_trace(
93
+ go.Scatter(
94
+ x=df_local["date"],
95
+ y=df_local[yoy_col],
96
+ name=f"{var} YoY (%)",
97
+ mode="lines+markers",
98
+ line=dict(width=2, dash="dash", color=base_color),
99
+ opacity=0.3,
100
+ hovertemplate=(f"<b>{var} YoY</b><br>Date: %{{x}}<br>Change: %{{y:.2f}}%<extra></extra>"),
101
+ ),
102
+ secondary_y=True
103
+ )
104
+
105
+ fig.update_layout(
106
+ title=title,
107
+ hovermode="closest",
108
+ legend=dict(x=0, y=-0.2, orientation="h", tracegroupgap=0),
109
+ )
110
+ fig.update_xaxes(title_text="Date")
111
+ fig.update_yaxes(title_text="Absolute Value", secondary_y=False)
112
+ fig.update_yaxes(title_text="YoY Change (%)", secondary_y=True)
113
+ return fig
114
+
115
+ # -------------------------------------------------------
116
+ # HELPER: ENHANCED INTERPRETATION TEXT
117
+ # -------------------------------------------------------
118
+ def interpret_financials(df: pd.DataFrame, metric_list: list[str], section_title: str, period: str) -> str:
119
+ existing_cols = [m for m in metric_list if m in df.columns]
120
+ if not existing_cols or df.empty:
121
+ return f"**{section_title}**: Data is not available for analysis."
122
+
123
+ df_valid = df[['date'] + existing_cols].dropna(subset=existing_cols, how='all')
124
+ if df_valid.empty:
125
+ return f"**{section_title}**: No valid data entries available."
126
+
127
+ df_valid = df_valid.sort_values("date")
128
+ latest_row = df_valid.iloc[-1]
129
+ latest_date = latest_row['date']
130
+ shift = 1 if period == "annual" else 4
131
+ period_type = "Year-over-Year" if period == "annual" else "Quarter-over-Quarter"
132
+
133
+ prior_row = df_valid.iloc[-1 - shift] if len(df_valid) > shift else None
134
+ prior_date = prior_row['date'] if prior_row is not None else None
135
+
136
+ values_only = df_valid[existing_cols].astype(float)
137
+ mean_vals = values_only.mean()
138
+ min_vals = values_only.min()
139
+ max_vals = values_only.max()
140
+ std_vals = values_only.std()
141
+
142
+ text = f"### {section_title}\n\n"
143
+ text += f"**Latest Data ({latest_date.date()}):** \n"
144
+ for col in existing_cols:
145
+ latest_val = latest_row[col]
146
+ text += f"- **{col.replace('_', ' ').title()}**: {latest_val:,.2f} \n" if pd.notna(latest_val) else f"- **{col.replace('_', ' ').title()}**: Data unavailable \n"
147
+
148
+ if prior_row is not None:
149
+ text += f"\n**{period_type} Change (vs. {prior_date.date()}):** \n"
150
+ for col in existing_cols:
151
+ latest_val = latest_row[col]
152
+ prior_val = prior_row[col]
153
+ if pd.notna(latest_val) and pd.notna(prior_val) and prior_val != 0:
154
+ pct_change = ((latest_val - prior_val) / abs(prior_val)) * 100
155
+ diff = latest_val - prior_val
156
+ direction = "increased" if diff > 0 else "decreased" if diff < 0 else "unchanged"
157
+ text += f"- **{col.replace('_', ' ').title()}**: {direction.capitalize()} by {abs(diff):,.2f} ({pct_change:+.1f}%) \n"
158
+ else:
159
+ text += f"- **{col.replace('_', ' ').title()}**: Insufficient data for comparison \n"
160
+
161
+ text += "\n**Historical Trends:** \n"
162
+ for col in existing_cols:
163
+ text += (f"- **{col.replace('_', ' ').title()}**: Mean = {mean_vals[col]:,.2f}, "
164
+ f"Min = {min_vals[col]:,.2f}, Max = {max_vals[col]:,.2f}, "
165
+ f"Std Dev = {std_vals[col]:,.2f} \n")
166
+
167
+ text += "\n**Investor Insights:** \n"
168
+ if section_title == "Revenue & Gross Profit":
169
+ text += (
170
+ "- Strong revenue growth paired with expanding gross profit margins signals operational efficiency and market strength. \n"
171
+ "- Declining trends may reflect competitive pressures or rising costs, impacting profitability. \n"
172
+ "- Volatility in these metrics could indicate cyclical demand or pricing instability. \n"
173
+ )
174
+ elif section_title == "Operating Expenses":
175
+ text += (
176
+ "- Rising expenses with stable revenue may erode margins, suggesting inefficiencies or investment in growth. \n"
177
+ "- Controlled or declining expenses reflect disciplined cost management. \n"
178
+ "- High variability could point to inconsistent operational strategies. \n"
179
+ )
180
+ elif section_title == "Net Income & Operating Income":
181
+ text += (
182
+ "- Consistent growth in operating and net income underscores sustainable earnings power. \n"
183
+ "- Divergence between operating income and net income may highlight tax or interest burdens. \n"
184
+ "- Sharp declines warrant investigation into cost structures or extraordinary items. \n"
185
+ )
186
+ elif section_title == "Earnings Per Share":
187
+ text += (
188
+ "- Rising EPS reflects enhanced shareholder value. \n"
189
+ "- Stagnant or falling EPS may signal dilution or profitability challenges. \n"
190
+ "- Compare diluted vs. basic EPS to assess the impact of potential equity issuance. \n"
191
+ )
192
+ elif section_title == "Assets":
193
+ text += (
194
+ "- Growth in total assets, especially liquid ones, indicates balance sheet strength and investment capacity. \n"
195
+ "- Declines may suggest asset sales or write-downs, potentially weakening financial flexibility. \n"
196
+ "- A balanced asset mix is key to supporting long-term growth. \n"
197
+ )
198
+ elif section_title == "Liabilities":
199
+ text += (
200
+ "- Increasing liabilities with stable assets raise leverage concerns. \n"
201
+ "- Controlled liability growth supports a stable capital structure. \n"
202
+ "- High short-term liabilities relative to cash may pressure liquidity. \n"
203
+ )
204
+ elif section_title == "Stockholders' Equity":
205
+ text += (
206
+ "- Rising equity reflects retained earnings growth or capital infusions. \n"
207
+ "- Declines may indicate losses or share repurchasing, affecting leverage ratios. \n"
208
+ "- Consistent equity growth enhances investor confidence. \n"
209
+ )
210
+ elif section_title == "Operating Activities":
211
+ text += (
212
+ "- Strong cash flow from operations indicates robust core business health. \n"
213
+ "- Negative or declining trends may reflect working capital issues. \n"
214
+ "- High depreciation relative to net income suggests significant non-cash adjustments. \n"
215
+ )
216
+ elif section_title == "Investing Activities":
217
+ text += (
218
+ "- Heavy investment in property or equipment signals long-term growth focus but may strain near-term cash. \n"
219
+ "- Positive cash from sales/maturities indicates strategic divestitures. \n"
220
+ "- Persistent negative flows suggest aggressive expansion. \n"
221
+ )
222
+ elif section_title == "Financing Activities":
223
+ text += (
224
+ "- Debt repayment or dividend increases reflect confidence in cash flows. \n"
225
+ "- Significant stock repurchasing may signal undervaluation or reduced growth. \n"
226
+ "- High financing inflows could indicate reliance on external capital. \n"
227
+ )
228
+ text += "\n*Recommendation*: Cross-reference these insights with industry benchmarks and broader market conditions."
229
+
230
+ return text
231
+
232
+ # -------------------------------------------------------
233
+ # PAGES
234
+ # -------------------------------------------------------
235
+ def page_income_statement(symbol: str, period: str):
236
+ key = f"income_{symbol}_{period}"
237
+ if key not in st.session_state.data_cache:
238
+ st.session_state.data_cache[key] = fetch_income_statement(symbol, period, API_KEY)
239
+ df = st.session_state.data_cache[key]
240
+
241
+ if df.empty:
242
+ st.error("No income statement data returned. Check symbol or period.")
243
+ return
244
+
245
+ st.success("Income Statement data loaded successfully.")
246
+ st.write("Charts display absolute values and period-over-period changes.")
247
+
248
+ st.subheader("1. Revenue & Gross Profit")
249
+ rev_vars = ["revenue", "grossProfit"]
250
+ fig_rev = create_dual_axis_figure(df, rev_vars, "Revenue & Gross Profit", period)
251
+ st.plotly_chart(fig_rev, use_container_width=True)
252
+ with st.expander("Interpretation"):
253
+ st.markdown(interpret_financials(df, rev_vars, "Revenue & Gross Profit", period))
254
+
255
+ st.subheader("2. Operating Expenses")
256
+ op_vars = ["researchAndDevelopmentExpenses", "sellingGeneralAndAdministrativeExpenses", "operatingExpenses"]
257
+ fig_op = create_dual_axis_figure(df, op_vars, "Operating Expenses", period)
258
+ st.plotly_chart(fig_op, use_container_width=True)
259
+ with st.expander("Interpretation"):
260
+ st.markdown(interpret_financials(df, op_vars, "Operating Expenses", period))
261
+
262
+ st.subheader("3. Net Income & Operating Income")
263
+ net_vars = ["netIncome", "operatingIncome", "incomeBeforeTax"]
264
+ fig_net = create_dual_axis_figure(df, net_vars, "Net Income & Operating Income", period)
265
+ st.plotly_chart(fig_net, use_container_width=True)
266
+ with st.expander("Interpretation"):
267
+ st.markdown(interpret_financials(df, net_vars, "Net Income & Operating Income", period))
268
+
269
+ st.subheader("4. Earnings Per Share")
270
+ eps_vars = ["eps", "epsdiluted"]
271
+ fig_eps = create_dual_axis_figure(df, eps_vars, "Earnings Per Share", period)
272
+ st.plotly_chart(fig_eps, use_container_width=True)
273
+ with st.expander("Interpretation"):
274
+ st.markdown(interpret_financials(df, eps_vars, "Earnings Per Share", period))
275
+
276
+ st.subheader("Complete Income Statement Data")
277
+ with st.expander("Show Complete Data"):
278
+ st.dataframe(df)
279
+
280
+ def page_balance_sheet(symbol: str, period: str):
281
+ key = f"balance_{symbol}_{period}"
282
+ if key not in st.session_state.data_cache:
283
+ st.session_state.data_cache[key] = fetch_balance_sheet(symbol, period, API_KEY)
284
+ df = st.session_state.data_cache[key]
285
+
286
+ if df.empty:
287
+ st.error("No balance sheet data returned. Check symbol or period.")
288
+ return
289
+
290
+ st.success("Balance Sheet data loaded successfully.")
291
+ st.write("Charts display absolute values and period-over-period changes.")
292
+
293
+ st.subheader("1. Assets")
294
+ asset_vars = ["cashAndShortTermInvestments", "totalCurrentAssets", "totalNonCurrentAssets", "totalAssets"]
295
+ fig_a = create_dual_axis_figure(df, asset_vars, "Assets", period)
296
+ st.plotly_chart(fig_a, use_container_width=True)
297
+ with st.expander("Interpretation"):
298
+ st.markdown(interpret_financials(df, asset_vars, "Assets", period))
299
+
300
+ st.subheader("2. Liabilities")
301
+ liability_vars = ["totalCurrentLiabilities", "totalNonCurrentLiabilities", "totalLiabilities"]
302
+ fig_l = create_dual_axis_figure(df, liability_vars, "Liabilities", period)
303
+ st.plotly_chart(fig_l, use_container_width=True)
304
+ with st.expander("Interpretation"):
305
+ st.markdown(interpret_financials(df, liability_vars, "Liabilities", period))
306
+
307
+ st.subheader("3. Stockholders' Equity")
308
+ equity_vars = ["commonStock", "retainedEarnings", "accumulatedOtherComprehensiveIncomeLoss", "totalStockholdersEquity"]
309
+ fig_e = create_dual_axis_figure(df, equity_vars, "Stockholders' Equity", period)
310
+ st.plotly_chart(fig_e, use_container_width=True)
311
+ with st.expander("Interpretation"):
312
+ st.markdown(interpret_financials(df, equity_vars, "Stockholders' Equity", period))
313
+
314
+ st.subheader("Complete Balance Sheet Data")
315
+ with st.expander("Show Complete Data"):
316
+ st.dataframe(df)
317
+
318
+ def page_cash_flow(symbol: str, period: str):
319
+ key = f"cash_{symbol}_{period}"
320
+ if key not in st.session_state.data_cache:
321
+ st.session_state.data_cache[key] = fetch_cash_flow(symbol, period, API_KEY)
322
+ df = st.session_state.data_cache[key]
323
+
324
+ if df.empty:
325
+ st.error("No cash flow data returned. Check symbol or period.")
326
+ return
327
+
328
+ st.success("Cash Flow data loaded successfully.")
329
+ st.write("Charts display absolute values and period-over-period changes.")
330
+
331
+ st.subheader("1. Operating Activities")
332
+ op_vars = ["netIncome", "depreciationAndAmortization", "changeInWorkingCapital", "netCashProvidedByOperatingActivities"]
333
+ fig_op = create_dual_axis_figure(df, op_vars, "Operating Activities", period)
334
+ st.plotly_chart(fig_op, use_container_width=True)
335
+ with st.expander("Interpretation"):
336
+ st.markdown(interpret_financials(df, op_vars, "Operating Activities", period))
337
+
338
+ st.subheader("2. Investing Activities")
339
+ inv_vars = ["investmentsInPropertyPlantAndEquipment", "purchasesOfInvestments", "salesMaturitiesOfInvestments", "netCashUsedForInvestingActivites"]
340
+ fig_inv = create_dual_axis_figure(df, inv_vars, "Investing Activities", period)
341
+ st.plotly_chart(fig_inv, use_container_width=True)
342
+ with st.expander("Interpretation"):
343
+ st.markdown(interpret_financials(df, inv_vars, "Investing Activities", period))
344
+
345
+ st.subheader("3. Financing Activities")
346
+ fin_vars = ["debtRepayment", "commonStockRepurchased", "dividendsPaid", "netCashUsedProvidedByFinancingActivities"]
347
+ fig_fin = create_dual_axis_figure(df, fin_vars, "Financing Activities", period)
348
+ st.plotly_chart(fig_fin, use_container_width=True)
349
+ with st.expander("Interpretation"):
350
+ st.markdown(interpret_financials(df, fin_vars, "Financing Activities", period))
351
+
352
+ st.subheader("Complete Cash Flow Data")
353
+ with st.expander("Show Complete Data"):
354
+ st.dataframe(df)
355
+
356
+ # -------------------------------------------------------
357
+ # MAIN
358
+ # -------------------------------------------------------
359
+ st.title("Financial Statements Analysis")
360
+ st.markdown("""
361
+ This tool presents key financial statements for your review.
362
+ It displays the Income Statement, Balance Sheet, and Cash Flow Statement.
363
+ Charts show absolute numbers on the left and changes over time on the right.
364
+ """)
365
+
366
+
367
+ # Sidebar: Navigation and Inputs
368
+ with st.sidebar.expander("Navigation", expanded=True):
369
+ selected_page = st.radio("Select Page", ["Income Statement", "Balance Sheet", "Cash Flow"], index=0)
370
+ st.session_state.page = selected_page
371
+
372
+ with st.sidebar.expander("Inputs", expanded=True):
373
+ symbol = st.text_input("Symbol or CIK", value="AAPL")
374
+ period = st.selectbox("Period", options=["annual", "quarter"])
375
+ run_button = st.button("Run Analysis")
376
+
377
+ # When run is pressed, update symbol/period and refresh only the active page.
378
+ if run_button:
379
+ st.session_state.symbol = symbol
380
+ st.session_state.period = period
381
+ current_page = st.session_state.page
382
+ if current_page == "Income Statement":
383
+ st.session_state.data_cache[f"income_{symbol}_{period}"] = fetch_income_statement(symbol, period, API_KEY)
384
+ elif current_page == "Balance Sheet":
385
+ st.session_state.data_cache[f"balance_{symbol}_{period}"] = fetch_balance_sheet(symbol, period, API_KEY)
386
+ elif current_page == "Cash Flow":
387
+ st.session_state.data_cache[f"cash_{symbol}_{period}"] = fetch_cash_flow(symbol, period, API_KEY)
388
+
389
+ # Retrieve the latest inputs from session state.
390
+ symbol = st.session_state.get('symbol', 'AAPL')
391
+ period = st.session_state.get('period', 'annual')
392
+ current_page = st.session_state.get('page', 'Income Statement')
393
+
394
+ if current_page == "Income Statement":
395
+ page_income_statement(symbol, period)
396
+ elif current_page == "Balance Sheet":
397
+ page_balance_sheet(symbol, period)
398
+ elif current_page == "Cash Flow":
399
+ page_cash_flow(symbol, period)
400
+
401
+ # Hide default Streamlit style
402
+ st.markdown(
403
+ """
404
+ <style>
405
+ #MainMenu {visibility: hidden;}
406
+ footer {visibility: hidden;}
407
+ </style>
408
+ """,
409
+ unsafe_allow_html=True
410
+ )