bohmian commited on
Commit
8741a2b
·
verified ·
1 Parent(s): 9ad7647

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +329 -37
src/streamlit_app.py CHANGED
@@ -1,40 +1,332 @@
1
- import altair as alt
2
- import numpy as np
3
  import pandas as pd
 
4
  import streamlit as st
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
  import pandas as pd
4
+ import plotly.graph_objects as go
5
  import streamlit as st
6
 
7
+ st.set_page_config(layout="wide")
8
+
9
+ # you need to save your FMP API Key into the respective environment variable to let it work
10
+ apikey = os.environ["FMP_API_KEY"]
11
+
12
+ def parse_json(url):
13
+ resp = requests.get(url)
14
+ resp.raise_for_status()
15
+ return pd.DataFrame(resp.json())
16
+
17
+ def fmt(val):
18
+ if abs(val) >= 1e9: return f"${val/1e9:.1f}B"
19
+ if abs(val) >= 1e6: return f"${val/1e6:.1f}M"
20
+ if abs(val) >= 1e3: return f"${val/1e3:.0f}K"
21
+ return f"${val:.0f}"
22
+
23
+ def draw_balance_sankey(balance_sheet, symbol, height, font_size):
24
+ # define all Sankey flows (left is source, right is target)
25
+ flows = [
26
+ # Current Assets
27
+ ("Cash and Cash Equivalents", "Total Current Assets", balance_sheet["cashAndCashEquivalents"]),
28
+ ("Short-Term Investments", "Total Current Assets", balance_sheet["shortTermInvestments"]),
29
+ ("Net Receivables", "Total Current Assets", balance_sheet["netReceivables"]),
30
+ ("Inventory", "Total Current Assets", balance_sheet["inventory"]),
31
+ ("Prepaids", "Total Current Assets", balance_sheet["prepaids"]),
32
+ # NOTE: Other Current Assets may sometimes overlap with some of the above lines
33
+ ("Other Current Assets", "Total Current Assets", balance_sheet["otherCurrentAssets"]),
34
+
35
+ # Non-Current Assets
36
+ ("Property, Plant and Equipment, Net", "Total Non-Current Assets", balance_sheet["propertyPlantEquipmentNet"]),
37
+ ("Goodwill", "Total Non-Current Assets", balance_sheet["goodwill"]),
38
+ ("Intangible Assets", "Total Non-Current Assets", balance_sheet["intangibleAssets"]),
39
+ ("Long-Term Investments", "Total Non-Current Assets", balance_sheet["longTermInvestments"]),
40
+ ("Tax Assets", "Total Non-Current Assets", balance_sheet["taxAssets"]),
41
+ ("Other Non-Current Assets", "Total Non-Current Assets", balance_sheet["otherNonCurrentAssets"]),
42
+
43
+ # ... to Total Assets
44
+ ("Total Current Assets", "Total Assets", balance_sheet["totalCurrentAssets"]),
45
+ ("Total Non-Current Assets", "Total Assets", balance_sheet["totalNonCurrentAssets"]),
46
+
47
+ # Total Assets to ...
48
+ ("Total Assets", "Total Liabilities", balance_sheet["totalLiabilities"]),
49
+ ("Total Assets", "Total Stockholders' Equity", balance_sheet["totalStockholdersEquity"]),
50
+
51
+ # Current Liabilities
52
+ ("Total Liabilities", "Total Current Liabilities", balance_sheet["totalCurrentLiabilities"]),
53
+ ("Total Current Liabilities", "Tax Payables", balance_sheet["taxPayables"]),
54
+ ("Total Current Liabilities", "Short-Term Debt", balance_sheet["shortTermDebt"]),
55
+ ("Total Current Liabilities", "Capital Lease Obligations (Current)", balance_sheet["capitalLeaseObligationsCurrent"]),
56
+ ("Total Current Liabilities", "Deferred Revenue (Current)", balance_sheet["deferredRevenue"]),
57
+ ("Total Current Liabilities", "Other Current Liabilities", balance_sheet["otherCurrentLiabilities"]),
58
+ ("Total Current Liabilities", "Accounts Payable", balance_sheet["accountPayables"]),
59
+ ("Total Current Liabilities", "Other Payables", balance_sheet["otherPayables"]),
60
+ ("Total Current Liabilities", "Accrued Expenses", balance_sheet["accruedExpenses"]),
61
+
62
+ # Non-Current Liabilities
63
+ ("Total Liabilities", "Total Non-Current Liabilities", balance_sheet["totalNonCurrentLiabilities"]),
64
+ ("Total Non-Current Liabilities", "Long-Term Debt", balance_sheet["longTermDebt"]),
65
+ ("Total Non-Current Liabilities", "Capital Lease Obligations (Non-Current)", balance_sheet["capitalLeaseObligationsNonCurrent"]),
66
+ ("Total Non-Current Liabilities", "Deferred Revenue (Non-Current)", balance_sheet["deferredRevenueNonCurrent"]),
67
+ ("Total Non-Current Liabilities", "Deferred Tax Liabilities (Non-Current)", balance_sheet["deferredTaxLiabilitiesNonCurrent"]),
68
+ ("Total Non-Current Liabilities", "Other Non-Current Liabilities", balance_sheet["otherNonCurrentLiabilities"]),
69
+
70
+ # Equity
71
+ ("Total Stockholders' Equity", "Common Stock", balance_sheet["commonStock"]),
72
+ ("Total Stockholders' Equity", "Retained Earnings", balance_sheet["retainedEarnings"]),
73
+ ("Total Stockholders' Equity", "Accumulated Other Comprehensive Income (Loss)", balance_sheet["accumulatedOtherComprehensiveIncomeLoss"]),
74
+ ("Total Stockholders' Equity", "Additional Paid-In Capital", balance_sheet["additionalPaidInCapital"]),
75
+ ("Total Stockholders' Equity", "Other Stockholders' Equity", balance_sheet["otherTotalStockholdersEquity"]),
76
+ ]
77
+
78
+
79
+ # need to adjust flow to make negative values easier to read
80
+ adjusted_flows = []
81
+ for src, tgt, val in flows:
82
+ if val >= 0:
83
+ # positive: keep direction, color green
84
+ adjusted_flows.append((src, tgt, val, 'rgba(50,200,50,0.6)'))
85
+ else:
86
+ # negative: reverse direction, color red
87
+ adjusted_flows.append((tgt, src, -val, 'rgba(200,50,50,0.6)'))
88
+
89
+ # for labelling later, first we store the source and target names
90
+ labels = []
91
+ for src, tgt, _ in flows:
92
+ if src not in labels: labels.append(src)
93
+ if tgt not in labels: labels.append(tgt)
94
+
95
+ # map label to its actual balance‐sheet value for annotation
96
+ node_values = {
97
+ "Cash and Cash Equivalents": balance_sheet["cashAndCashEquivalents"],
98
+ "Short-Term Investments": balance_sheet["shortTermInvestments"],
99
+ "Net Receivables": balance_sheet["netReceivables"],
100
+ "Inventory": balance_sheet["inventory"],
101
+ "Prepaids": balance_sheet["prepaids"],
102
+ "Other Current Assets": balance_sheet["otherCurrentAssets"],
103
+ "Total Current Assets": balance_sheet["totalCurrentAssets"],
104
+ "Property, Plant and Equipment, Net": balance_sheet["propertyPlantEquipmentNet"],
105
+ "Goodwill": balance_sheet["goodwill"],
106
+ "Intangible Assets": balance_sheet["intangibleAssets"],
107
+ "Long-Term Investments": balance_sheet["longTermInvestments"],
108
+ "Tax Assets": balance_sheet["taxAssets"],
109
+ "Other Non-Current Assets": balance_sheet["otherNonCurrentAssets"],
110
+ "Total Non-Current Assets": balance_sheet["totalNonCurrentAssets"],
111
+ "Total Assets": balance_sheet["totalAssets"],
112
+
113
+ "Total Liabilities": balance_sheet["totalLiabilities"],
114
+ "Total Current Liabilities": balance_sheet["totalCurrentLiabilities"],
115
+ "Tax Payables": balance_sheet["taxPayables"],
116
+ "Short-Term Debt": balance_sheet["shortTermDebt"],
117
+ "Capital Lease Obligations (Current)": balance_sheet["capitalLeaseObligationsCurrent"],
118
+ "Deferred Revenue (Current)": balance_sheet["deferredRevenue"],
119
+ "Other Current Liabilities": balance_sheet["otherCurrentLiabilities"],
120
+
121
+ "Accounts Payable": balance_sheet["accountPayables"],
122
+ "Other Payables": balance_sheet["otherPayables"],
123
+ "Accrued Expenses": balance_sheet["accruedExpenses"],
124
+
125
+ "Total Non-Current Liabilities": balance_sheet["totalNonCurrentLiabilities"],
126
+ "Long-Term Debt": balance_sheet["longTermDebt"],
127
+ "Capital Lease Obligations (Non-Current)": balance_sheet["capitalLeaseObligationsNonCurrent"],
128
+ "Deferred Revenue (Non-Current)": balance_sheet["deferredRevenueNonCurrent"],
129
+ "Deferred Tax Liabilities (Non-Current)": balance_sheet["deferredTaxLiabilitiesNonCurrent"],
130
+ "Other Non-Current Liabilities": balance_sheet["otherNonCurrentLiabilities"],
131
+
132
+ "Total Stockholders' Equity": balance_sheet["totalStockholdersEquity"],
133
+ "Common Stock": balance_sheet["commonStock"],
134
+ "Retained Earnings": balance_sheet["retainedEarnings"],
135
+ "Accumulated Other Comprehensive Income (Loss)": balance_sheet["accumulatedOtherComprehensiveIncomeLoss"],
136
+ "Additional Paid-In Capital": balance_sheet["additionalPaidInCapital"],
137
+ "Other Stockholders' Equity": balance_sheet["otherTotalStockholdersEquity"],
138
+ }
139
+
140
+ # for formatting, annotate labels with $ amounts and take care of billions, millions, thousands
141
+ def fmt(val):
142
+ if abs(val) >= 1e9: return f"${val/1e9:.1f}B"
143
+ if abs(val) >= 1e6: return f"${val/1e6:.1f}M"
144
+ if abs(val) >= 1e3: return f"${val/1e3:.0f}K"
145
+ return f"${val:.0f}"
146
+
147
+ # put the sorce and target values in labels
148
+ labels = []
149
+ for s, t, _, _ in adjusted_flows:
150
+ if s not in labels: labels.append(s)
151
+ if t not in labels: labels.append(t)
152
+
153
+ idx = {label:i for i,label in enumerate(labels)}
154
+ source = [ idx[s] for s, t, _, _ in adjusted_flows ] # index of sources for sankey input
155
+ target = [ idx[t] for s, t, _, _ in adjusted_flows ] # index of target for sankey input
156
+ value = [ v for _, _, v, _ in adjusted_flows ]
157
+ colors = [ c for _, _, _, c in adjusted_flows ]
158
+
159
+ label_with_values = []
160
+ for label in labels:
161
+ val = node_values[label]
162
+ base = label.replace(" (Current)", "")\
163
+ .replace(" (Non-Current)", "") # saves some printing space
164
+ if val < 0:
165
+ base += " [NEGATIVE]" # just to make negatives more obvious in the label
166
+ label_with_values.append(f"{base} ({fmt(val)})")
167
+
168
+ fig = go.Figure(go.Sankey(
169
+ arrangement="snap",
170
+ node = dict(label=label_with_values, pad=15, thickness=20),
171
+ link = dict(source=source, target=target, value=value, color=colors)
172
+ ))
173
+ fig.update_layout(
174
+ title_text=f"Balance Sheet Sankey — {symbol}",
175
+ height=height,
176
+ font_size=font_size
177
+ )
178
+ return fig
179
+
180
+ def draw_income_sankey(income_statement, symbol, height, font_size):
181
+ flows = [
182
+ # Revenue─
183
+ ("Revenue", "Cost of Revenue", income_statement["costOfRevenue"]),
184
+ ("Revenue", "Gross Profit", income_statement["grossProfit"]),
185
+
186
+ # Gross Profit
187
+ ("Gross Profit", "Operating Income", income_statement["operatingIncome"]),
188
+ ("Gross Profit", "Operating Expenses", income_statement["operatingExpenses"]),
189
+
190
+ # Operating Expenses
191
+ ("Operating Expenses", "Research & Development Expenses", income_statement["researchAndDevelopmentExpenses"]),
192
+ # ("Operating Expenses", "General & Administrative Expenses", income_statement["generalAndAdministrativeExpenses"]), # already in SG&A
193
+ # ("Operating Expenses", "Selling & Marketing Expenses", income_statement["sellingAndMarketingExpenses"]), # already in SG&A
194
+ ("Operating Expenses", "SG&A Expenses", income_statement["sellingGeneralAndAdministrativeExpenses"]),
195
+ ("Operating Expenses", "Other Operating Expenses", income_statement["otherExpenses"]),
196
+
197
+ # Pretax Income
198
+ ("Pretax Income", "Income Tax Expense", income_statement["incomeTaxExpense"]),
199
+ ("Pretax Income", "Net Income", income_statement["netIncome"]),
200
+ ("Pretax Income", "Interest Expense", income_statement["interestExpense"]),
201
+ # this value is recorded as negative in API, but we do not need to reverse the flow like in balance sheet
202
+ # because it decreases the pretax income so we put it together at the same side with all the tax expenses
203
+ ("Pretax Income", "Non-Operating Income Excl. Interest", -income_statement["nonOperatingIncomeExcludingInterest"]),
204
+ ("Pretax Income", "Total Other Income & Expenses Net", income_statement["totalOtherIncomeExpensesNet"]),
205
+ ("Pretax Income", "Other Adjustments to Net Income", income_statement["otherAdjustmentsToNetIncome"]),
206
+
207
+ # Other Income that goes into Pretax Income
208
+ ("Operating Income", "Pretax Income", income_statement["operatingIncome"]),
209
+ ("Net Interest Income", "Pretax Income", income_statement["netInterestIncome"]),
210
+ ("Interest Income", "Pretax Income", income_statement["interestIncome"]),
211
+ ]
212
+
213
+
214
+ # need to adjust flow to make negative values easier to read
215
+ adjusted_flows = []
216
+ for src, tgt, val in flows:
217
+ if val >= 0:
218
+ # positive: keep direction, color green
219
+ adjusted_flows.append((src, tgt, val, 'rgba(50,200,50,0.6)'))
220
+ else:
221
+ # negative: reverse direction, color red
222
+ adjusted_flows.append((tgt, src, -val, 'rgba(200,50,50,0.6)'))
223
+
224
+ # for labelling later, first we store the source and target names
225
+ labels = []
226
+ for src, tgt, _ in flows:
227
+ if src not in labels: labels.append(src)
228
+ if tgt not in labels: labels.append(tgt)
229
+
230
+ # map label to its actual balance‐sheet value for annotation
231
+ node_values = {
232
+ "Revenue": income_statement["revenue"],
233
+ "Cost of Revenue": income_statement["costOfRevenue"],
234
+ "Gross Profit": income_statement["grossProfit"],
235
+
236
+ "Operating Income": income_statement["operatingIncome"],
237
+ "Operating Expenses": income_statement["operatingExpenses"],
238
+ "Research & Development Expenses": income_statement["researchAndDevelopmentExpenses"],
239
+ #"General & Administrative Expenses": income_statement["generalAndAdministrativeExpenses"], # already in SG&A
240
+ #"Selling & Marketing Expenses": income_statement["sellingAndMarketingExpenses"], # already in SG&A
241
+ "SG&A Expenses": income_statement["sellingGeneralAndAdministrativeExpenses"],
242
+ "Other Operating Expenses": income_statement["otherExpenses"],
243
+
244
+ "Net Interest Income": income_statement["netInterestIncome"],
245
+ "Interest Income": income_statement["interestIncome"],
246
+ "Interest Expense": income_statement["interestExpense"],
247
+ "Non-Operating Income Excl. Interest":-income_statement["nonOperatingIncomeExcludingInterest"],
248
+ "Total Other Income & Expenses Net": income_statement["totalOtherIncomeExpensesNet"],
249
+
250
+ "Pretax Income": income_statement["incomeBeforeTax"],
251
+ "Income Tax Expense": income_statement["incomeTaxExpense"],
252
+ "Net Income": income_statement["netIncome"],
253
+ "Other Adjustments to Net Income": income_statement["otherAdjustmentsToNetIncome"],
254
+ "Bottom Line Net Income": income_statement["bottomLineNetIncome"],
255
+ }
256
+
257
+ # for formatting, annotate labels with $ amounts and take care of billions, millions, thousands
258
+ def fmt(val):
259
+ if abs(val) >= 1e9: return f"${val/1e9:.1f}B"
260
+ if abs(val) >= 1e6: return f"${val/1e6:.1f}M"
261
+ if abs(val) >= 1e3: return f"${val/1e3:.0f}K"
262
+ return f"${val:.0f}"
263
+
264
+ # put the sorce and target values in labels
265
+ labels = []
266
+ for s, t, _, _ in adjusted_flows:
267
+ if s not in labels: labels.append(s)
268
+ if t not in labels: labels.append(t)
269
+
270
+ idx = {label:i for i,label in enumerate(labels)}
271
+ source = [ idx[s] for s, t, _, _ in adjusted_flows ] # index of sources for sankey input
272
+ target = [ idx[t] for s, t, _, _ in adjusted_flows ] # index of target for sankey input
273
+ value = [ v for _, _, v, _ in adjusted_flows ]
274
+ colors = [ c for _, _, _, c in adjusted_flows ]
275
+
276
+ label_with_values = []
277
+ for label in labels:
278
+ val = node_values[label]
279
+ base = label.replace(" (Current)", "")\
280
+ .replace(" (Non-Current)", "") # saves some printing space
281
+ if val < 0:
282
+ base += " [NEGATIVE]" # just to make negatives more obvious in the label
283
+ label_with_values.append(f"{base} ({fmt(val)})")
284
+
285
+ fig = go.Figure(go.Sankey(
286
+ arrangement="snap",
287
+ node = dict(label=label_with_values, pad=15, thickness=20),
288
+ link = dict(source=source, target=target, value=value, color=colors)
289
+ ))
290
+ fig.update_layout(
291
+ title_text=f"Income Statement Sankey — {symbol}",
292
+ height=height,
293
+ font_size=font_size
294
+ )
295
+ return fig
296
+
297
+ st.title("Financial Sankeys")
298
+
299
+ symbol = st.sidebar.text_input("Ticker symbol", "AMZN").upper()
300
+
301
+ # sidebar controls
302
+ bs_height = st.sidebar.slider("Balance Sheet height", 500, 1500, 800)
303
+ bs_font = st.sidebar.slider("Balance Sheet font size", 5, 15, 10)
304
+ is_height = st.sidebar.slider("Income Statement height", 500, 1500, 600)
305
+ is_font = st.sidebar.slider("Income Statement font size", 5, 15, 10)
306
+
307
+ # where the data came from
308
+ st.sidebar.markdown("## [Financial Modeling Prep API](https://site.financialmodelingprep.com/?utm_source=medium&utm_medium=medium&utm_campaign=damian8)\
309
+ \n\nFinancial statements are obtained from the FinancialModelingPrep API, feel free to sign up\
310
+ [here](https://site.financialmodelingprep.com/?utm_source=medium&utm_medium=medium&utm_campaign=damian8)\
311
+ if you wish.")
312
+
313
+ if symbol:
314
+ # Balance Sheet
315
+ st.header(f"Balance Sheet — {symbol}")
316
+ try:
317
+ df_bs = parse_json(f"https://financialmodelingprep.com/stable/balance-sheet-statement?symbol={symbol}&apikey={apikey}")
318
+ balance_sheet = df_bs.iloc[0]
319
+ fig_bs = draw_balance_sankey(balance_sheet, symbol.upper(), bs_height, bs_font)
320
+ st.plotly_chart(fig_bs, use_container_width=True)
321
+ except Exception as e:
322
+ st.error(f"Failed to fetch balance sheet: {e}")
323
+
324
+ # Income Statement
325
+ st.header(f"Income Statement — {symbol}")
326
+ try:
327
+ df_is = parse_json(f"https://financialmodelingprep.com/stable/income-statement?symbol={symbol}&apikey={apikey}")
328
+ income_statement = df_is.iloc[0]
329
+ fig_is = draw_income_sankey(income_statement, symbol.upper(), is_height, is_font)
330
+ st.plotly_chart(fig_is, use_container_width=True)
331
+ except Exception as e:
332
+ st.error(f"Failed to fetch income statement: {e}")