QuantumLearner commited on
Commit
23f1f3e
·
verified ·
1 Parent(s): d73dc51

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +677 -0
app.py ADDED
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import pandas as pd
4
+ import yfinance as yf
5
+ from plotly.subplots import make_subplots
6
+ import plotly.graph_objects as go
7
+
8
+ # Global API key (hidden from users)
9
+
10
+ API_KEY = os.getenv("FMP_API_KEY")
11
+
12
+ # -------------------------------
13
+ # Helper function to fetch JSON safely
14
+ # -------------------------------
15
+ def safe_get_json(url, log_list=None, dimension_label=""):
16
+ try:
17
+ response = requests.get(url)
18
+ data = response.json()
19
+ return data
20
+ except Exception:
21
+ msg = f"Unable to retrieve historical data for {dimension_label}."
22
+ if log_list is not None:
23
+ log_list.append(msg)
24
+ else:
25
+ st.error("An error occurred while retrieving historical data. Please try again later.")
26
+ return None
27
+
28
+ # -------------------------------
29
+ # Dimension Functions
30
+ # -------------------------------
31
+ def dimension_1_positive_roa(symbol, years_back=1, log_list=None):
32
+ limit_needed = years_back + 1
33
+ income_url = (
34
+ f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}"
35
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
36
+ )
37
+ balance_url = (
38
+ f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}"
39
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
40
+ )
41
+ income_data = safe_get_json(income_url, log_list, "Dimension 1")
42
+ balance_data = safe_get_json(balance_url, log_list, "Dimension 1")
43
+ if income_data is None or balance_data is None:
44
+ return []
45
+ if len(income_data) < limit_needed or len(balance_data) < limit_needed:
46
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
47
+ if log_list is not None:
48
+ log_list.append(msg)
49
+ else:
50
+ st.error(msg)
51
+ return []
52
+ results = []
53
+ for i in range(years_back):
54
+ current_income = income_data[i]
55
+ current_balance = balance_data[i]
56
+ year_or_date = current_income.get("calendarYear") or current_income.get("date", f"N/A_{i}")
57
+ net_income_current = current_income.get("netIncome", 0)
58
+ ta_current = current_balance.get("totalAssets", 0)
59
+ ta_previous = balance_data[i+1].get("totalAssets", 0) if i+1 < len(balance_data) else 0
60
+ avg_assets = (ta_current + ta_previous) / 2 if ta_previous else 0
61
+ roa_current = net_income_current / avg_assets if avg_assets else 0
62
+ score = 1 if roa_current > 0 else 0
63
+ log_message = (
64
+ f"Dimension 1 (Positive ROA) | Year={year_or_date}: {score} => "
65
+ f"NetIncome={net_income_current}, AvgAssets={int(avg_assets)}, ROA={roa_current:.4f}"
66
+ )
67
+ if log_list is not None:
68
+ log_list.append(log_message)
69
+ results.append({"year": str(year_or_date), "score": score})
70
+ return results
71
+
72
+ def dimension_2_positive_cfo(symbol, years_back=1, log_list=None):
73
+ limit_needed = years_back
74
+ cf_url = (
75
+ f"https://financialmodelingprep.com/api/v3/cash-flow-statement/{symbol}"
76
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
77
+ )
78
+ cf_data = safe_get_json(cf_url, log_list, "Dimension 2")
79
+ if cf_data is None:
80
+ return []
81
+ if len(cf_data) < limit_needed:
82
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
83
+ if log_list is not None:
84
+ log_list.append(msg)
85
+ return []
86
+ results = []
87
+ for i in range(years_back):
88
+ record = cf_data[i]
89
+ year_or_date = record.get("calendarYear") or record.get("date", f"N/A_{i}")
90
+ cfo_current = record.get("operatingCashFlow", 0)
91
+ score = 1 if cfo_current > 0 else 0
92
+ log_message = f"Dimension 2 (Positive CFO) | Year={year_or_date}: {score} => CFO={cfo_current}"
93
+ if log_list is not None:
94
+ log_list.append(log_message)
95
+ results.append({"year": str(year_or_date), "score": score})
96
+ return results
97
+
98
+ def dimension_3_improved_roa(symbol, years_back=1, log_list=None):
99
+ limit_needed = years_back + 1
100
+ income_url = (
101
+ f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}"
102
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
103
+ )
104
+ balance_url = (
105
+ f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}"
106
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
107
+ )
108
+ income_data = safe_get_json(income_url, log_list, "Dimension 3")
109
+ balance_data = safe_get_json(balance_url, log_list, "Dimension 3")
110
+ if income_data is None or balance_data is None:
111
+ return []
112
+ if len(income_data) < limit_needed or len(balance_data) < limit_needed:
113
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
114
+ if log_list is not None:
115
+ log_list.append(msg)
116
+ return []
117
+ results = []
118
+ for i in range(years_back):
119
+ current_income = income_data[i]
120
+ current_balance = balance_data[i]
121
+ year_or_date = current_income.get("calendarYear") or current_income.get("date", f"N/A_{i}")
122
+ net_income_current = current_income.get("netIncome", 0)
123
+ ta_current = current_balance.get("totalAssets", 0)
124
+ if i+1 < len(income_data):
125
+ net_income_previous = income_data[i+1].get("netIncome", 0)
126
+ ta_previous = balance_data[i+1].get("totalAssets", 0)
127
+ else:
128
+ net_income_previous = 0
129
+ ta_previous = 0
130
+ avg_current = (ta_current + ta_previous) / 2 if ta_previous else 0
131
+ roa_current = net_income_current / avg_current if avg_current else 0
132
+ roa_previous = (net_income_previous / ta_previous) if ta_previous else 0
133
+ score = 1 if roa_current > roa_previous else 0
134
+ log_message = (
135
+ f"Dimension 3 (ROA Improvement) | Year={year_or_date}: {score} => "
136
+ f"ROA_current={roa_current:.4f}, ROA_previous={roa_previous:.4f}"
137
+ )
138
+ if log_list is not None:
139
+ log_list.append(log_message)
140
+ results.append({"year": str(year_or_date), "score": score})
141
+ return results
142
+
143
+ def dimension_4_cfo_exceeds_net_income(symbol, years_back=1, log_list=None):
144
+ limit_needed = years_back
145
+ cf_url = (
146
+ f"https://financialmodelingprep.com/api/v3/cash-flow-statement/{symbol}"
147
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
148
+ )
149
+ income_url = (
150
+ f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}"
151
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
152
+ )
153
+ cf_data = safe_get_json(cf_url, log_list, "Dimension 4")
154
+ income_data = safe_get_json(income_url, log_list, "Dimension 4")
155
+ if cf_data is None or income_data is None:
156
+ return []
157
+ if len(cf_data) < limit_needed or len(income_data) < limit_needed:
158
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
159
+ if log_list is not None:
160
+ log_list.append(msg)
161
+ return []
162
+ results = []
163
+ for i in range(years_back):
164
+ c = cf_data[i]
165
+ inc = income_data[i]
166
+ year_or_date = c.get("calendarYear") or c.get("date", f"N/A_{i}")
167
+ cfo_current = c.get("operatingCashFlow", 0)
168
+ net_income_current = inc.get("netIncome", 0)
169
+ score = 1 if cfo_current > net_income_current else 0
170
+ log_message = (
171
+ f"Dimension 4 (CFO > Net Income) | Year={year_or_date}: {score} => "
172
+ f"CFO={cfo_current}, NetIncome={net_income_current}"
173
+ )
174
+ if log_list is not None:
175
+ log_list.append(log_message)
176
+ results.append({"year": str(year_or_date), "score": score})
177
+ return results
178
+
179
+ def dimension_5_lower_leverage(symbol, years_back=1, log_list=None):
180
+ limit_needed = years_back + 1
181
+ bal_url = (
182
+ f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}"
183
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
184
+ )
185
+ balance_data = safe_get_json(bal_url, log_list, "Dimension 5")
186
+ if balance_data is None:
187
+ return []
188
+ if len(balance_data) < limit_needed:
189
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
190
+ if log_list is not None:
191
+ log_list.append(msg)
192
+ return []
193
+ results = []
194
+ for i in range(years_back):
195
+ current_bal = balance_data[i]
196
+ year_or_date = current_bal.get("calendarYear") or current_bal.get("date", f"N/A_{i}")
197
+ ltd_current = current_bal.get("longTermDebt", 0)
198
+ ta_current = current_bal.get("totalAssets", 0)
199
+ if i+1 < len(balance_data):
200
+ ltd_previous = balance_data[i+1].get("longTermDebt", 0)
201
+ ta_previous = balance_data[i+1].get("totalAssets", 0)
202
+ else:
203
+ ltd_previous = 0
204
+ ta_previous = 0
205
+ ratio_current = ltd_current / ta_current if ta_current else 0
206
+ ratio_previous = ltd_previous / ta_previous if ta_previous else 0
207
+ score = 1 if ratio_current < ratio_previous else 0
208
+ log_message = (
209
+ f"Dimension 5 (Lower Debt Ratio) | Year={year_or_date}: {score} => "
210
+ f"DebtRatio_current={ratio_current:.4f}, DebtRatio_previous={ratio_previous:.4f}"
211
+ )
212
+ if log_list is not None:
213
+ log_list.append(log_message)
214
+ results.append({"year": str(year_or_date), "score": score})
215
+ return results
216
+
217
+ def dimension_6_higher_current_ratio(symbol, years_back=1, log_list=None):
218
+ limit_needed = years_back + 1
219
+ bal_url = (
220
+ f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}"
221
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
222
+ )
223
+ balance_data = safe_get_json(bal_url, log_list, "Dimension 6")
224
+ if balance_data is None:
225
+ return []
226
+ if len(balance_data) < limit_needed:
227
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
228
+ if log_list is not None:
229
+ log_list.append(msg)
230
+ return []
231
+ results = []
232
+ for i in range(years_back):
233
+ current_bal = balance_data[i]
234
+ year_or_date = current_bal.get("calendarYear") or current_bal.get("date", f"N/A_{i}")
235
+ ca_current = current_bal.get("totalCurrentAssets", 0)
236
+ cl_current = current_bal.get("totalCurrentLiabilities", 0)
237
+ cr_current = ca_current / cl_current if cl_current else 0
238
+ if i+1 < len(balance_data):
239
+ ca_previous = balance_data[i+1].get("totalCurrentAssets", 0)
240
+ cl_previous = balance_data[i+1].get("totalCurrentLiabilities", 0)
241
+ cr_previous = ca_previous / cl_previous if cl_previous else 0
242
+ else:
243
+ cr_previous = 0
244
+ score = 1 if cr_current > cr_previous else 0
245
+ log_message = (
246
+ f"Dimension 6 (Higher Current Ratio) | Year={year_or_date}: {score} => "
247
+ f"CR_current={cr_current:.4f}, CR_previous={cr_previous:.4f}"
248
+ )
249
+ if log_list is not None:
250
+ log_list.append(log_message)
251
+ results.append({"year": str(year_or_date), "score": score})
252
+ return results
253
+
254
+ def dimension_7_no_new_shares(symbol, years_back=1, log_list=None):
255
+ limit_needed = years_back + 1
256
+ inc_url = (
257
+ f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}"
258
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
259
+ )
260
+ inc_data = safe_get_json(inc_url, log_list, "Dimension 7")
261
+ if inc_data is None:
262
+ return []
263
+ if len(inc_data) < limit_needed:
264
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
265
+ if log_list is not None:
266
+ log_list.append(msg)
267
+ return []
268
+ results = []
269
+ for i in range(years_back):
270
+ current_inc = inc_data[i]
271
+ year_or_date = current_inc.get("calendarYear") or current_inc.get("date", f"N/A_{i}")
272
+ shares_current = current_inc.get("weightedAverageShsOut", 0)
273
+ shares_previous = inc_data[i+1].get("weightedAverageShsOut", 0) if i+1 < len(inc_data) else 0
274
+ score = 1 if shares_current <= shares_previous else 0
275
+ log_message = (
276
+ f"Dimension 7 (No New Shares) | Year={year_or_date}: {score} => "
277
+ f"Shares_current={shares_current}, Shares_previous={shares_previous}"
278
+ )
279
+ if log_list is not None:
280
+ log_list.append(log_message)
281
+ results.append({"year": str(year_or_date), "score": score})
282
+ return results
283
+
284
+ def dimension_8_improved_gross_margin(symbol, years_back=1, log_list=None):
285
+ limit_needed = years_back + 1
286
+ inc_url = (
287
+ f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}"
288
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
289
+ )
290
+ inc_data = safe_get_json(inc_url, log_list, "Dimension 8")
291
+ if inc_data is None:
292
+ return []
293
+ if len(inc_data) < limit_needed:
294
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
295
+ if log_list is not None:
296
+ log_list.append(msg)
297
+ return []
298
+ results = []
299
+ for i in range(years_back):
300
+ current_inc = inc_data[i]
301
+ year_or_date = current_inc.get("calendarYear") or current_inc.get("date", f"N/A_{i}")
302
+ rev_current = current_inc.get("revenue", 0)
303
+ gp_current = current_inc.get("grossProfit", 0)
304
+ gm_current = gp_current / rev_current if rev_current else 0
305
+ if i+1 < len(inc_data):
306
+ rev_previous = inc_data[i+1].get("revenue", 0)
307
+ gp_previous = inc_data[i+1].get("grossProfit", 0)
308
+ gm_previous = gp_previous / rev_previous if rev_previous else 0
309
+ else:
310
+ gm_previous = 0
311
+ score = 1 if gm_current > gm_previous else 0
312
+ log_message = (
313
+ f"Dimension 8 (Gross Margin Up) | Year={year_or_date}: {score} => "
314
+ f"GM_current={gm_current:.4f}, GM_previous={gm_previous:.4f}"
315
+ )
316
+ if log_list is not None:
317
+ log_list.append(log_message)
318
+ results.append({"year": str(year_or_date), "score": score})
319
+ return results
320
+
321
+ def dimension_9_improved_ato(symbol, years_back=1, log_list=None):
322
+ limit_needed = years_back + 1
323
+ inc_url = (
324
+ f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}"
325
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
326
+ )
327
+ bal_url = (
328
+ f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}"
329
+ f"?limit={limit_needed}&period=annual&apikey={API_KEY}"
330
+ )
331
+ inc_data = safe_get_json(inc_url, log_list, "Dimension 9")
332
+ bal_data = safe_get_json(bal_url, log_list, "Dimension 9")
333
+ if inc_data is None or bal_data is None:
334
+ return []
335
+ if len(inc_data) < limit_needed or len(bal_data) < limit_needed:
336
+ msg = f"Not enough historical data available to calculate the metric for {years_back} year(s)."
337
+ if log_list is not None:
338
+ log_list.append(msg)
339
+ return []
340
+ results = []
341
+ for i in range(years_back):
342
+ inc_current = inc_data[i]
343
+ bal_current = bal_data[i]
344
+ year_or_date = inc_current.get("calendarYear") or inc_current.get("date", f"N/A_{i}")
345
+ rev_current = inc_current.get("revenue", 0)
346
+ ta_current = bal_current.get("totalAssets", 0)
347
+ ta_prev_for_cur = bal_data[i+1].get("totalAssets", 0) if i+1 < len(bal_data) else 0
348
+ avg_assets_current = (ta_current + ta_prev_for_cur) / 2 if ta_prev_for_cur else 0
349
+ ato_current = rev_current / avg_assets_current if avg_assets_current else 0
350
+ if i+1 < len(inc_data) and i+2 < len(bal_data):
351
+ rev_previous = inc_data[i+1].get("revenue", 0)
352
+ ta_previous = bal_data[i+1].get("totalAssets", 0)
353
+ ato_previous = rev_previous / ta_previous if ta_previous else 0
354
+ else:
355
+ ato_previous = 0
356
+ score = 1 if ato_current > ato_previous else 0
357
+ log_message = (
358
+ f"Dimension 9 (Asset Turnover Up) | Year={year_or_date}: {score} => "
359
+ f"ATO_current={ato_current:.4f}, ATO_previous={ato_previous:.4f}"
360
+ )
361
+ if log_list is not None:
362
+ log_list.append(log_message)
363
+ results.append({"year": str(year_or_date), "score": score})
364
+ return results
365
+
366
+ # -------------------------------
367
+ # Aggregator Function: Combine all dimensions over time
368
+ # -------------------------------
369
+ def calculate_piotroski_scores_over_time(symbol, years_back=5, log_list=None):
370
+ d1_list = dimension_1_positive_roa(symbol, years_back, log_list=log_list)
371
+ d2_list = dimension_2_positive_cfo(symbol, years_back, log_list=log_list)
372
+ d3_list = dimension_3_improved_roa(symbol, years_back, log_list=log_list)
373
+ d4_list = dimension_4_cfo_exceeds_net_income(symbol, years_back, log_list=log_list)
374
+ d5_list = dimension_5_lower_leverage(symbol, years_back, log_list=log_list)
375
+ d6_list = dimension_6_higher_current_ratio(symbol, years_back, log_list=log_list)
376
+ d7_list = dimension_7_no_new_shares(symbol, years_back, log_list=log_list)
377
+ d8_list = dimension_8_improved_gross_margin(symbol, years_back, log_list=log_list)
378
+ d9_list = dimension_9_improved_ato(symbol, years_back, log_list=log_list)
379
+
380
+ rows = []
381
+ for i in range(years_back):
382
+ year_str = d1_list[i]["year"] if i < len(d1_list) else f"N/A_{i}"
383
+ dim1 = d1_list[i]["score"] if i < len(d1_list) else 0
384
+ dim2 = d2_list[i]["score"] if i < len(d2_list) else 0
385
+ dim3 = d3_list[i]["score"] if i < len(d3_list) else 0
386
+ dim4 = d4_list[i]["score"] if i < len(d4_list) else 0
387
+ dim5 = d5_list[i]["score"] if i < len(d5_list) else 0
388
+ dim6 = d6_list[i]["score"] if i < len(d6_list) else 0
389
+ dim7 = d7_list[i]["score"] if i < len(d7_list) else 0
390
+ dim8 = d8_list[i]["score"] if i < len(d8_list) else 0
391
+ dim9 = d9_list[i]["score"] if i < len(d9_list) else 0
392
+ total_score = sum([dim1, dim2, dim3, dim4, dim5, dim6, dim7, dim8, dim9])
393
+ rows.append({
394
+ "year": year_str,
395
+ "dim1_roa": dim1,
396
+ "dim2_cfo": dim2,
397
+ "dim3_roa_improvement": dim3,
398
+ "dim4_cfo_over_ni": dim4,
399
+ "dim5_lower_debt_ratio": dim5,
400
+ "dim6_higher_current_ratio": dim6,
401
+ "dim7_no_new_shares": dim7,
402
+ "dim8_gross_margin_up": dim8,
403
+ "dim9_asset_turnover_up": dim9,
404
+ "total_score": total_score
405
+ })
406
+ df = pd.DataFrame(rows)
407
+ return df
408
+
409
+ # -------------------------------
410
+ # Fetch annual stock prices using yfinance
411
+ # -------------------------------
412
+ def fetch_stock_prices_for_years(symbol, df_scores):
413
+ try:
414
+ df_scores["year_int"] = df_scores["year"].astype(int)
415
+ except Exception:
416
+ st.error("Error processing year values.")
417
+ return df_scores
418
+
419
+ min_year = df_scores["year_int"].min()
420
+ max_year = df_scores["year_int"].max()
421
+ start_date = f"{min_year}-01-01"
422
+ end_date = f"{max_year}-12-31"
423
+ try:
424
+ ticker_obj = yf.Ticker(symbol)
425
+ hist = ticker_obj.history(start=start_date, end=end_date)
426
+ except Exception:
427
+ st.error("Error retrieving stock price data.")
428
+ return df_scores
429
+
430
+ year_to_price = {}
431
+ for y in df_scores["year_int"].unique():
432
+ try:
433
+ data_y = hist.loc[str(y)] if str(y) in hist.index.strftime("%Y") else pd.DataFrame()
434
+ except Exception:
435
+ data_y = pd.DataFrame()
436
+ if data_y.empty:
437
+ year_to_price[y] = None
438
+ else:
439
+ last_close = data_y["Close"].iloc[-1]
440
+ year_to_price[y] = float(f"{last_close:.2f}")
441
+ df_scores["stock_price"] = df_scores["year_int"].map(year_to_price)
442
+ return df_scores
443
+
444
+ # -------------------------------
445
+ # Set wide layout and page title
446
+ # -------------------------------
447
+ st.set_page_config(page_title="Piotroski Score Analysis", layout="wide")
448
+ st.title("Piotroski Score Analysis")
449
+ st.markdown(
450
+ """
451
+ This tool calculates the Piotroski F-Score over time for a given stock to investigate its financial health and performance trends.
452
+ Simply adjust the parameters in the sidebar and click **Run Analysis** to view detailed scores, its decomposition, and interactive visualizations.
453
+ """
454
+ )
455
+
456
+ # -------------------------------
457
+ # Explanation of Calculations Expander
458
+ # -------------------------------
459
+ with st.expander("F-Score Calculations", expanded=False):
460
+ st.markdown(
461
+ """
462
+ The Piotroski F-Score is a nine-point system designed to identify financially strong companies.
463
+ Each of the nine dimensions is binary (1 if favorable, 0 if not) and falls into groups like Profitability, Leverage & Liquidity, and Operational Efficiency.
464
+ """
465
+ )
466
+ st.markdown("##### 1. Positive Return on Assets (ROA)")
467
+ st.markdown(
468
+ """
469
+ Measures how effectively a company uses its assets to generate net income.
470
+ Calculated as:
471
+ """
472
+ )
473
+ st.latex(r"\text{ROA} = \frac{\text{Net Income}}{\frac{\text{Total Assets}_{\text{current}} + \text{Total Assets}_{\text{previous}}}{2}}")
474
+ st.markdown("A positive ROA indicates the company is profitable relative to its asset base.")
475
+
476
+ st.markdown("##### 2. Positive Operating Cash Flow (CFO)")
477
+ st.markdown(
478
+ """
479
+ Evaluates whether the company generates cash from its core operations.
480
+ Expressed simply as:
481
+ """
482
+ )
483
+ st.latex(r"\text{CFO} > 0")
484
+ st.markdown("A positive CFO suggests sustainable business operations.")
485
+
486
+ st.markdown("##### 3. Improvement in ROA")
487
+ st.markdown(
488
+ """
489
+ Compares the current year's ROA to the previous year's to indicate improving profitability.
490
+ In formula form:
491
+ """
492
+ )
493
+ st.latex(r"\Delta\text{ROA} = \text{ROA}_{\text{current}} - \text{ROA}_{\text{previous}} > 0")
494
+ st.markdown("If the difference is positive, the score is 1.")
495
+
496
+ st.markdown("##### 4. CFO Exceeds Net Income")
497
+ st.markdown(
498
+ """
499
+ Checks that the cash flow from operations is greater than net income, implying high earnings quality.
500
+ Expressed as:
501
+ """
502
+ )
503
+ st.latex(r"\text{CFO} > \text{Net Income}")
504
+ st.markdown("If true, the indicator receives a score of 1.")
505
+
506
+ st.markdown("##### 5. Decrease in Long-Term Debt Ratio")
507
+ st.markdown(
508
+ """
509
+ Evaluates whether the company is reducing its financial leverage over time.
510
+ Calculated as:
511
+ """
512
+ )
513
+ st.latex(r"\text{Debt Ratio} = \frac{\text{Long-Term Debt}}{\text{Total Assets}}")
514
+ st.markdown("A lower debt ratio in the current year versus the previous year scores 1.")
515
+
516
+ st.markdown("##### 6. Improvement in Current Ratio")
517
+ st.markdown(
518
+ """
519
+ Assesses short-term liquidity by comparing current assets to current liabilities.
520
+ Calculated as:
521
+ """
522
+ )
523
+ st.latex(r"\text{Current Ratio} = \frac{\text{Total Current Assets}}{\text{Total Current Liabilities}}")
524
+ st.markdown("An increase in the current ratio year-over-year signals stronger liquidity.")
525
+
526
+ st.markdown("##### 7. No New Shares Issued")
527
+ st.markdown(
528
+ """
529
+ Checks that the weighted average shares outstanding have not increased, avoiding dilution.
530
+ Expressed as:
531
+ """
532
+ )
533
+ st.latex(r"\text{Weighted Average Shares}_{\text{current}} \leq \text{Weighted Average Shares}_{\text{previous}}")
534
+ st.markdown("If true, the score is 1.")
535
+
536
+ st.markdown("##### 8. Improvement in Gross Margin")
537
+ st.markdown("Gross Margin is defined as:")
538
+ st.latex(r"\text{Gross Margin} = \frac{\text{Gross Profit}}{\text{Revenue}}")
539
+ st.markdown("An increase in gross margin indicates better cost management or pricing power.")
540
+
541
+ st.markdown("##### 9. Improvement in Asset Turnover")
542
+ st.markdown(
543
+ """
544
+ Measures how efficiently a company uses its assets to generate revenue.
545
+ Calculated as:
546
+ """
547
+ )
548
+ st.latex(r"\text{Asset Turnover} = \frac{\text{Revenue}}{\frac{\text{Total Assets}_{\text{current}} + \text{Total Assets}_{\text{previous}}}{2}}")
549
+ st.markdown("An increase in asset turnover indicates more efficient use of assets.")
550
+
551
+ # -------------------------------
552
+ # Sidebar: Parameters Expander
553
+ # -------------------------------
554
+ with st.sidebar.expander("Parameters", expanded=True):
555
+ ticker = st.text_input("Ticker Symbol", value="MSFT",
556
+ help="Enter the stock ticker symbol (e.g., MSFT)")
557
+ years_back = st.slider("Number of Years", min_value=1, max_value=20, value=10, help="Set how many past years to analyze")
558
+ run_analysis = st.button("Run Analysis")
559
+
560
+ # -------------------------------
561
+ # Run the analysis on button click
562
+ # -------------------------------
563
+ if run_analysis:
564
+ with st.spinner("Running analysis. Please wait..."):
565
+ raw_logs = []
566
+ df_scores = calculate_piotroski_scores_over_time(ticker, years_back, log_list=raw_logs)
567
+ df_scores = fetch_stock_prices_for_years(ticker, df_scores)
568
+ dim_cols = [
569
+ "dim1_roa", "dim2_cfo", "dim3_roa_improvement",
570
+ "dim4_cfo_over_ni", "dim5_lower_debt_ratio",
571
+ "dim6_higher_current_ratio", "dim7_no_new_shares",
572
+ "dim8_gross_margin_up", "dim9_asset_turnover_up"
573
+ ]
574
+ df_plot = df_scores.sort_values(by="year", ascending=True)
575
+
576
+ # Create Plotly figure with secondary y-axis
577
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
578
+ for col in dim_cols:
579
+ fig.add_trace(
580
+ go.Bar(
581
+ x=df_plot["year"],
582
+ y=df_plot[col],
583
+ name=col,
584
+ text=df_plot[col],
585
+ textposition="inside"
586
+ ),
587
+ secondary_y=False
588
+ )
589
+ # Add annotations for total score above each bar
590
+ for idx, row in df_plot.iterrows():
591
+ fig.add_annotation(
592
+ x=row["year"],
593
+ y=row["total_score"] + 0.1,
594
+ text=f"Score={int(row['total_score'])}",
595
+ showarrow=False,
596
+ font=dict(color="black", size=10)
597
+ )
598
+ fig.add_trace(
599
+ go.Scatter(
600
+ x=df_plot["year"],
601
+ y=df_plot["stock_price"],
602
+ mode="lines+markers",
603
+ name="Stock Price",
604
+ marker=dict(color="red"),
605
+ line=dict(width=2)
606
+ ),
607
+ secondary_y=True
608
+ )
609
+ fig.update_xaxes(
610
+ tickmode='array',
611
+ tickvals=df_plot["year"].tolist(),
612
+ ticktext=df_plot["year"].tolist()
613
+ )
614
+ fig.update_layout(
615
+ height=800,
616
+ barmode="stack",
617
+ title_text=f"Piotroski Dimensions for {ticker} with Stock Price",
618
+ xaxis_title="Year",
619
+ yaxis_title="Dimension Scores (Stacked)",
620
+ legend=dict(orientation="h", yanchor="bottom", y=1.20),
621
+ margin=dict(b=150)
622
+ )
623
+ fig.update_yaxes(title_text="Stock Price (USD)", secondary_y=True)
624
+
625
+ st.subheader("Results")
626
+ with st.expander("Raw Calculation Logs", expanded=False):
627
+ st.markdown("Below are the raw logs for each metric's calculation:")
628
+ for log in raw_logs:
629
+ st.text(log)
630
+
631
+ st.markdown("##### DataFrame")
632
+ with st.expander("DataFrame", expanded=False):
633
+ st.dataframe(df_scores)
634
+
635
+ st.markdown("##### Time Series Plot")
636
+ st.plotly_chart(fig, use_container_width=True)
637
+
638
+ st.markdown("##### Interpretation of the results")
639
+ with st.expander("interpretation of results", expanded=False):
640
+ for idx, row in df_scores.iterrows():
641
+ year_label = row["year"]
642
+ st.markdown(f"##### {year_label}")
643
+ weaknesses = []
644
+ if row["dim1_roa"] == 0:
645
+ weaknesses.append("ROA is not positive. This may indicate lower profit relative to assets.")
646
+ if row["dim2_cfo"] == 0:
647
+ weaknesses.append("CFO is negative or zero. Operations did not produce sufficient cash flow.")
648
+ if row["dim3_roa_improvement"] == 0:
649
+ weaknesses.append("ROA did not improve. Asset profitability may be stagnant.")
650
+ if row["dim4_cfo_over_ni"] == 0:
651
+ weaknesses.append("CFO is not higher than net income. Earnings quality could be weak.")
652
+ if row["dim5_lower_debt_ratio"] == 0:
653
+ weaknesses.append("Debt ratio did not decrease. Leverage has not improved.")
654
+ if row["dim6_higher_current_ratio"] == 0:
655
+ weaknesses.append("Current ratio is not higher than before. Short-term liquidity did not improve.")
656
+ if row["dim7_no_new_shares"] == 0:
657
+ weaknesses.append("Shares outstanding increased. This may dilute existing shareholders.")
658
+ if row["dim8_gross_margin_up"] == 0:
659
+ weaknesses.append("Gross margin did not rise. Cost or pricing factors may need attention.")
660
+ if row["dim9_asset_turnover_up"] == 0:
661
+ weaknesses.append("Asset turnover did not increase. Efficiency in using assets could be better.")
662
+
663
+ if weaknesses:
664
+ weakness_text = "; ".join(weaknesses)
665
+ st.markdown(f"**Key Weaknesses:** {weakness_text}")
666
+ else:
667
+ st.markdown("No identified weaknesses in this year's metrics. Scores suggest strong performance.")
668
+ st.markdown("---")
669
+
670
+
671
+ hide_streamlit_style = """
672
+ <style>
673
+ #MainMenu {visibility: hidden;}
674
+ footer {visibility: hidden;}
675
+ </style>
676
+ """
677
+ st.markdown(hide_streamlit_style, unsafe_allow_html=True)