gluo88 commited on
Commit
a3d0cbf
·
verified ·
1 Parent(s): aea9f49

Update performance.py

Browse files

temparary version for years_list = [1, 2, 3, 5, 10, 15, 20, 25, 30, 40, 50, 60]

Files changed (1) hide show
  1. performance.py +316 -208
performance.py CHANGED
@@ -1,176 +1,186 @@
1
  '''
2
- Example 8 for using yfinance
3
-
4
  Calculate annual, trailing, cumumlative, and CAGR returns for multiple stocks.
5
  * The start date can be an arbitrary date. The default is the current date.
6
  * annual return is displayed from the default current day, or an arbitrary given
7
- day (except for Feb 29 for leap year) TODO-fix
 
8
  * trailing, cumumlative returns are currently displayed from the month boundary (last day of Month)
9
  prior to the given date.
10
  * However, trailing, cumumlative returns can be displayed
11
  from any date, which can be not at the month boundary (last day of Month),
12
  by minor change of setting calculation_end_date_for_others_str = calculation_end_date_str.
13
  prior to the given date in the function "calculation_response(message, history)"
14
-
15
  Author: Gang Luo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  '''
17
- script_version = '(2024-01-26.1)'
 
18
  import gradio as gr
19
  import yfinance as yf
20
  import pandas as pd
21
  import numpy as np
22
  from datetime import datetime, timedelta
23
  import pytz
 
24
  #==============================================================================
25
 
26
  print_yearly_total_return = True
27
- num_years_calculation=32 # total years for calculation
28
 
29
  # Define a list of years to calculate the trailing returns, cumulative returns, and so on
30
  # remove the row of current year row since it is not a full year.
31
- years_list = [1, 2, 3, 5, 10, 15, 20, 25, 30]
 
32
 
33
  # Set the stock tickers list
34
- tickers_lists = [["qqq","hxq.to","spy", "vfv.to","xiu.to", "xbb.to","xcb.to","xhb.to"], #0
35
- ["spy", "vfv.to", "vgg.to", "zlu.to", "xiu.to", "vdy.to", "xfn.to", "ry.to", "td.to", "na.to",
36
- "slf.to", "gwo.to", "bce.to", "t.to", "rci-b.to", "enb.to", "trp.to", "zlb.to", "cp.to"], #1
37
- ["spy","vfv.to", "xiu.to", "zeb.to", "xfn.to", "na.to","ry.to", "bmo.to","bns.to", "td.to", "cm.to", "cwb.to",
38
- "slf.to", "gwo.to", "bce.to", "t.to", "rci-b.to", "enb.to", "trp.to", "xdv.to","cdz.to","vdy.to"], #2
39
- ["qqq","spy", "vfv.to", "vgg.to","zlu.to","msft","AAPL","goog","AMZN","NVDA","meta","tsla","shop.to","hxq.to"], #3
40
- ["^GSPC","spy","voo","ivv", "tpu-u.to","vfv.to", "zsp.to","hxs.to","tpu.to","xus.to", "xsp.to","^IXIC","qqq","hxq.to","^GSPTSE","xic.to","xiu.to","xfn.to", "fie.to"], #4
41
- ["^IXIC","ONEQ","CIBR","QQJG", "qqq", "spy", "vfv.to", "HXQ.to", "ZQQ.to", "XQQ.to", "QQC.to"], #5
42
- ["goog", "msft", "^GSPC"]
 
 
 
 
 
 
43
  ]
44
 
45
  #==============================================================================
46
- # Part 1: fetch retrieve yearly total returns by yfinance & display
47
- # Function to fetch data from yfinance and extract yearly total returns#
48
- # annual return calculation can start at any given day
49
- def get_annual_returns_df(ticker, calculation_end_date_str):
50
- # Get the historical data for the given ticker
51
- stock = yf.Ticker(ticker)
52
- calculation_end_date=pd.to_datetime(calculation_end_date_str).tz_localize('America/New_York')
53
  try:
54
  '''
55
- 'try' statement for handlingy the exception error of stock.history that a ticker is not yet at stock market,
56
- For example, "shop.to" is not there in 2012
57
- '''
58
- stock_history=stock.history(period="max")["Close"]
59
  '''
60
- Between the start and end days in stock_history variable, there are some missing days where there are no corresponding rows.
61
- Add rows of missing days such that the values of column "Close" are set to be the value of the closest earlier day's
62
- value, by using date_range to create full range without any missing date.
63
- '''
64
- # Create a DataFrame with a complete date range
65
- date_range = pd.date_range(start=stock_history.index.min(), end=stock_history.index.max(), freq='D')
66
- complete_stock_history = pd.DataFrame(index=date_range)
67
- # Merge the complete DataFrame with the original stock_history
68
- complete_stock_history = complete_stock_history.merge(stock_history, how='left', left_index=True, right_index=True)
69
- complete_stock_history['Close'] = complete_stock_history['Close'].ffill() # fill the newy added rows with previous day value
70
- '''
71
- Filter out the rows that matches the month and date of calculation_end_date, which are the ends of
72
- annual periods from the calculation_end_date.
73
- '''
74
- # Filter out rows with dates newer than calculation_end_date
75
- filtered_stock_history = complete_stock_history[complete_stock_history.index <= calculation_end_date]
76
- #print(filtered_stock_history)
77
- target_month=filtered_stock_history.index.max().month
78
- target_day=filtered_stock_history.index.max().day
79
- #print("target_month", target_month, "target_day",target_day, "start_year", filtered_stock_history.index.max().year)
80
- annual_returns = filtered_stock_history[(filtered_stock_history.index.month == target_month)
81
- & (filtered_stock_history.index.day ==target_day)]
82
- annual_returns_percent = annual_returns.pct_change().dropna()
83
  except:
84
  return pd.DataFrame()
85
  else:
86
- annual_returns_df = pd.DataFrame(annual_returns_percent, columns=['Close'])
87
- annual_returns_df.rename(columns={'Close': ticker}, inplace=True)
88
- return annual_returns_df
89
-
90
- # Function to fetch data from yfinance and extract yearly total returns
91
- # annual return calculation starts at only yaer end boundary, i.e, Dec 31,
92
- # by resample('A')
93
- def get_annual_returns_year_boundary_df(ticker, calculation_end_date_str):
94
- # Get the historical data for the given ticker
95
- stock = yf.Ticker(ticker)
96
- calculation_end_date = datetime.strptime(calculation_end_date_str, "%Y-%m-%d")
97
- calculation_start_date_str = (calculation_end_date
98
- - timedelta(days=num_years_calculation * 365)).strftime("%Y-%m-%d")
99
 
100
- try:
101
- '''
102
- 1. 'try' statement for handlingy the exception error of stock.history that a ticker is not yet at stock market,
103
- For example, "shop.to" is not there in 2012
104
- 2. The row with the latest day from .history(.., end='end_day_date') is the day prior to end_day_date. Therefore,
105
- let end=the expected end day plus one day.
106
- '''
107
- calculation_end_date_plus_1day_str = (calculation_end_date + timedelta(days=1)).strftime("%Y-%m-%d")
108
- annual_returns_history=stock.history(start=calculation_start_date_str,end=calculation_end_date_plus_1day_str)["Close"]
109
-
110
- #print("debug get_annual_returns_df ", ticker, annual_returns_history)
111
- # For 'A', 'Y', see https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
112
- ffilled_history=annual_returns = annual_returns_history.resample('A').ffill()
113
- #print(ffilled_history)
114
- annual_returns = ffilled_history.pct_change().dropna()
115
- #annual_returns = annual_returns_history.resample('A').ffill().pct_change().dropna()
116
- #print("debug get_annual_returns_df after resample()", ticker, calculation_end_date, "\n", annual_returns)
117
- except:
118
- return pd.DataFrame()
119
- else:
120
- annual_returns_df = pd.DataFrame(annual_returns, columns=['Close'])
121
- annual_returns_df.rename(columns={'Close': ticker}, inplace=True)
122
- return annual_returns_df
123
 
124
- #----------------------------------------------------------------------------------
125
- # handling a list of tickers by calling the functions (either get_annual_returns_df
126
- # get_annual_returns_year_boundary_df) that handle single tickers
127
- def get_annual_returns_tickers_common_df(tickers, calculation_end_date_str, annual_returns_func_df):
128
- # Create an empty DataFrame to store all tickers' total returns
129
- all_tickers_returns_df = pd.DataFrame()
130
 
131
- # Loop through each ticker in the list
132
- for ticker in tickers:
133
- ticker_returns_df = annual_returns_func_df(ticker, calculation_end_date_str)
134
- if not ticker_returns_df.empty:
135
- if all_tickers_returns_df.empty:
136
- all_tickers_returns_df = ticker_returns_df
137
- else:
138
- '''
139
- When running in huggingface, pd.concat changed the index order of ticker_returns_df
140
- when ticker_returns_df has more rows than all_tickers_returns_df. However, it is ok
141
- running in colab. Therefore, use pd.merge to replace pd.concat.
142
- all_tickers_returns_df = pd.concat([all_tickers_returns_df, ticker_returns_df],axis=1,join='outer') # Concatenate DataFrames
143
- all_tickers_returns_df.sort_index() # index may be changed when running in huggingface
144
- '''
145
- all_tickers_returns_df = pd.merge(all_tickers_returns_df, ticker_returns_df,
146
- left_index=True, right_index=True, how='outer')
147
- else:
148
- # New column with NaN values
149
- new_column_name = ticker
150
- new_column_values = [None] * len(all_tickers_returns_df)
151
- new_column = pd.DataFrame({new_column_name: new_column_values}, index=all_tickers_returns_df.index)
152
- # Concatenate the new column to the original DataFrame
153
- all_tickers_returns_df = pd.concat([all_tickers_returns_df, new_column], axis=1)
154
- #return date_label_conversion_strip_time(all_tickers_returns_df, calculation_end_date_str)
155
- return all_tickers_returns_df
 
 
156
 
157
- def get_annual_returns_tickers_df(tickers, calculation_end_date_str):
158
- return get_annual_returns_tickers_common_df(tickers, calculation_end_date_str,
159
- get_annual_returns_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
- def get_annual_returns_tickers_year_boundary_df(tickers, calculation_end_date_str):
162
- return get_annual_returns_tickers_common_df(tickers, calculation_end_date_str,
163
- get_annual_returns_year_boundary_df)
 
 
 
 
 
164
 
165
  #==============================================================================
166
- # Part 2: calculate the annualized trailing total return from the data generated in step 1 & display
167
  # Define a function to calculate the annualized trailing total return for a given number of years
168
  def get_trailing_return(ticker, data, years):
169
  # Get the total return values for the last n years
170
  trailing_data = data[ticker].tail(years)
171
  # Check if there are empty values within years
172
  if trailing_data.isna().any():
173
- return "N/A"
174
  # Check if there are valid total return values for all years
175
  if len(trailing_data) == years:
176
  # Convert the percentage strings to numeric values
@@ -185,7 +195,7 @@ def get_trailing_return(ticker, data, years):
185
  annualized_trailing_return = annualized_trailing_return.round(2)
186
  return annualized_trailing_return
187
  else:
188
- return "N/A"
189
 
190
  # Define a function to Loop through the list and print the trailing returns for each num_years
191
  def get_trailing_return_column(ticker, annual_returns_df):
@@ -199,13 +209,13 @@ def get_trailing_return_column(ticker, annual_returns_df):
199
  trailing_return_column[f"{num_years}-Year"] = trailing_return
200
  else:
201
  print(f"Data not available for {ticker}. Skipping.")
202
- trailing_return_column[f"{num_years}-Year"] = "N/A"
203
  return trailing_return_column
204
 
205
  # Create an empty DataFrame to store all tickers' trailing returns
206
- def get_trailing_return_all(tickers, annual_returns_df):
207
  all_tickers_trailing_returns_df = pd.DataFrame(index=years_list)
208
-
209
  # Loop through each ticker in the list
210
  for ticker in tickers:
211
  trailing_returns = get_trailing_return_column(ticker, annual_returns_df)
@@ -214,7 +224,7 @@ def get_trailing_return_all(tickers, annual_returns_df):
214
  return all_tickers_trailing_returns_df
215
 
216
  #==============================================================================
217
- # Part 3: calculate the cumulative return from the data (all_tickers_returns_df) generated in part 1 & display
218
  # Define a function to calculate the cumulative return for a given number of years from a ticker
219
  def get_cumulative_return(ticker, data, years):
220
  # Calculate the cumulative return
@@ -231,9 +241,10 @@ def get_cumulative_return_column(ticker, annual_returns_df):
231
  cumulative_returns[years] = cumulative_return.iloc[-1]
232
  return cumulative_returns
233
 
234
- def get_cumulative_return_all(tickers, annual_returns_df):
235
  # Create an empty DataFrame with years_list as the index for cumulative returns
236
  all_tickers_cumulative_returns_df = pd.DataFrame(index=years_list)
 
237
  # Loop through each ticker in the list
238
  for ticker in tickers:
239
  cumulative_returns = get_cumulative_return_column(ticker, annual_returns_df)
@@ -242,7 +253,7 @@ def get_cumulative_return_all(tickers, annual_returns_df):
242
  return all_tickers_cumulative_returns_df
243
 
244
  #==============================================================================
245
- # Part 4: calculate the CAGR (Compound Annual Growth Rate) from the data
246
  # in all_tickers_cumulative_returns_df generated earlier & display
247
  # Define a function to calculate the CAGR from the cumulative value and the years
248
  def calculate_cagr(value, years):
@@ -264,46 +275,6 @@ def get_cagr_return_all(all_tickers_cumulative_returns_df):
264
  all_tickers_cagrs_df = all_tickers_cumulative_returns_df.apply(lambda x: calculate_cagr(x, x.index), axis=0)
265
  return all_tickers_cagrs_df
266
 
267
- #==============================================================================
268
- # Part 5: utility functions
269
- # get the last trading day of S&P 500 in string format
270
- def get_last_trading_day():
271
- # Get today's date, use .strftime("%Y-%m-%d") to convert to a string
272
- today_date_str=datetime.now(pytz.timezone('America/New_York')).date().strftime("%Y-%m-%d")
273
- stock = yf.Ticker("^GSPC") # S&P 500 (^GSPC) ticker
274
- # search and see yfinance_BUG_1 NOTE in this file
275
- history_df=stock.history(period="max", end=today_date_str)["Close"]
276
- last_trading_day_str = history_df.index.max().date().strftime("%Y-%m-%d")
277
- return last_trading_day_str
278
-
279
- def str_to_integer(integer_str):
280
- try:
281
- integer_number = int(integer_str)
282
- return integer_number
283
- except ValueError:
284
- return -1
285
-
286
- # validate the date string
287
- def is_valid_date(date_string):
288
- try:
289
- # Attempt to parse the date string
290
- datetime.strptime(date_string, "%Y-%m-%d")
291
- return True
292
- except ValueError:
293
- # Raised when the date string is not in the expected format
294
- return False
295
-
296
- def date_label_conversion_strip_time(all_tickers_returns_df, calculation_end_date_str):
297
- all_tickers_returns_df.index=all_tickers_returns_df.index.date
298
- all_tickers_returns_df.index.name='date'
299
- # print("debug get_annual_returns_tickers_df", all_tickers_returns_df)
300
- # Convert calculation_end_date_str to a datetime object, replace the index's mon/day portion of date
301
- end_date_datetime_obj = datetime.strptime(calculation_end_date_str, "%Y-%m-%d")
302
- all_tickers_returns_df.index = all_tickers_returns_df.index.map(
303
- lambda x: x.replace(month=end_date_datetime_obj.month,
304
- day=end_date_datetime_obj.day))
305
- return all_tickers_returns_df
306
-
307
  #==============================================================================
308
  # Part 6:
309
  # single ticker's Prices, Returns,Dividends, good for verifying whether "Adj Close" is correct.
@@ -317,14 +288,21 @@ def date_label_conversion_strip_time(all_tickers_returns_df, calculation_end_dat
317
  def get_yearly_single_stock_data(ticker):
318
  stock = yf.Ticker(ticker)
319
  #-------- mainly for downloading 'Dividends'
320
- history = stock.history(period="max")
321
  dividend_history=history['Dividends']
322
  dividend_history.index=dividend_history.index.date
323
 
324
  #-------- mainly for downloading 'Close','Adj Close'
325
- dld_history=yf.download(ticker, period="max")
326
  dld_history=dld_history[['Close','Adj Close']]
327
  dld_history.rename(columns={'Adj Close': 'AdjClose'}, inplace=True)
 
 
 
 
 
 
 
328
  date_range = pd.date_range(start=dld_history.index.min(), end=dld_history.index.max(), freq='D')
329
  complete_history = pd.DataFrame(index=date_range)
330
 
@@ -368,18 +346,58 @@ def get_yearly_single_stock_data(ticker):
368
  return yearly_data
369
 
370
  #==============================================================================
371
- # Part 7: gradio handling - Input command handling and display in web page
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  help_info_str="Input Formats:\n \
374
  1. ticker list....................Example: spy vfv.to xiu.to xic.to xfn.to ry.to \n \
375
- 2. One of default ticker list, a number between 1 and 5....Example: 0, or 1, ...,5 \n \
376
  3. CalculationEndDate as prefix. Example: 2020-12-31 2 \n \
377
  .........................................2020-12-31 spy vfv.to xiu.to xic.to xfn.to ry.to \n \
378
  4. single ticker: Dividend/Close/AdjClose/Return/TotalReturn/CalReturn(by Close/Dividends). @1 spy \n \
379
- note: daily adjusted close data are from Yahoo Finance. "
380
 
381
- # Gradio Web interface
382
- def calculation_response(message, history):
383
  # if there is no input, display help information
384
  if message=="":
385
  return help_info_str
@@ -429,10 +447,17 @@ def calculation_response(message, history):
429
  # calculation_end_date_for_others are for trailing and cumulative returns
430
  calculation_end_date_for_others_str=calculation_end_date_month_boundary_date_str
431
 
432
- ''' TODO handling Feb 29 of leap year.
433
- Check if involved dates (in calculation_end_date_str and calculation_end_date_for_others_str),
434
- Feb 28 will be used to replace Feb 29 for calculation
 
435
  '''
 
 
 
 
 
 
436
  #................End
437
 
438
  # Check whether numebr 0, 1, 2, .. is selected for using a default ticker list
@@ -443,47 +468,130 @@ def calculation_response(message, history):
443
  # if no tickers were set, display help information
444
  if len(tickers)==0:
445
  return help_info_str
 
 
446
 
447
  #*********************************************************************************
448
- # Calculating Annual, Trailing, Cumulative, and CAGR & generating html for display
449
- # annual_returns - at year end boundard, to be displayed
 
 
 
 
 
 
 
 
450
  output_string = f"\nAnnual Total Return (%) as {calculation_end_date_str}\n"
451
- output_dataframe = get_annual_returns_tickers_year_boundary_df(tickers, calculation_end_date_str)
 
 
452
  output_dataframe = output_dataframe.round(4)*100
453
- output_dataframe.index=output_dataframe.index.date
454
  # Assuming your DataFrame is named output_dataframe
455
  last_date = output_dataframe.index[-1]
456
  output_dataframe = output_dataframe.rename(index={last_date: calculation_end_date_str})
457
  # Convert the DataFrame to HTML, Combine the expected string outputs
458
- output_html1 = output_string + output_dataframe.to_html()
 
459
 
460
  # annual_returns - at any given day, for calculating trailing and cumulative returns, not to be displayed
461
- annual_returns_dataframe=get_annual_returns_tickers_df(tickers, calculation_end_date_for_others_str)
 
 
462
 
463
  # Trailing Return
464
- output_string2 = f"\nTrailing Total Return (%) as {calculation_end_date_for_others_str}\n"
465
- output_dataframe2=get_trailing_return_all(tickers, annual_returns_dataframe)
 
 
 
 
466
  # Insert an empty to align the ticker symbols with annual return display
467
- output_dataframe2.insert(0, "--------", " ")
468
- output_dataframe2.index.name="years"
469
- output_html2=output_string2 + output_dataframe2.to_html()
470
-
471
- # Cumulative Return
472
- output_string3 = f"\nCumulative Return (%) as {calculation_end_date_for_others_str}\n"
473
- cumulative_return_all_dataframe=get_cumulative_return_all(tickers, annual_returns_dataframe)
474
- output_dataframe3=cumulative_return_all_dataframe.round(4)*100
475
- output_dataframe3.index.name="years"
476
  output_html3=output_string3 + output_dataframe3.to_html()
477
-
478
- # CAGR Return
479
- output_string4 = f"\nCompound Annual Growth Rate (CAGR) (%) as {calculation_end_date_for_others_str}\n"
480
- output_dataframe4=get_cagr_return_all (cumulative_return_all_dataframe)
481
- output_dataframe4=output_dataframe4.round(4)*100
 
 
482
  output_html4=output_string4 + output_dataframe4.to_html()
483
 
484
- #output_html = output_html1 + output_html2 + output_html3 + output_html4
485
- output_html = output_html1 + output_html2 + output_html3
 
 
 
 
 
 
 
 
486
  return output_html
487
 
488
- demo = gr.ChatInterface(calculation_response)
489
- demo.launch(debug=False, share=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  '''
2
+ Example 9 for using yfinance
 
3
  Calculate annual, trailing, cumumlative, and CAGR returns for multiple stocks.
4
  * The start date can be an arbitrary date. The default is the current date.
5
  * annual return is displayed from the default current day, or an arbitrary given
6
+ day (except for Feb 29 for leap year)
7
+ For leap years, use Feb 28 to replace Feb 29 as simplification & approximation
8
  * trailing, cumumlative returns are currently displayed from the month boundary (last day of Month)
9
  prior to the given date.
10
  * However, trailing, cumumlative returns can be displayed
11
  from any date, which can be not at the month boundary (last day of Month),
12
  by minor change of setting calculation_end_date_for_others_str = calculation_end_date_str.
13
  prior to the given date in the function "calculation_response(message, history)"
 
14
  Author: Gang Luo
15
+
16
+ yfinance References:
17
+ code: https://github.com/ranaroussi/yfinance
18
+ project: https://pypi.org/project/yfinance/
19
+ Guide: https://algotrading101.com/learn/yfinance-guide/
20
+
21
+
22
+ Revision history:
23
+ 2025-02.23.1444: fixing issues of missing "Adj Close" in yf.download and yf.Ticker("AAPL"),
24
+ caused by (https://github.com/ranaroussi/yfinance/issues/2283) which is introduced by
25
+ yfinance version 0.2.54 (released on Feb 18, 2025 ).
26
+ 2025-02.23.1655: further fix for the issues from (https://github.com/ranaroussi/yfinance/issues/2283).
27
+ The "Adj Close" column is missing from yf.download since yf.download default changed
28
+ from auto_adjust=False to auto_adjust=True. When auto_adjust=True, column Close is actually Adj Close and
29
+ Adj Close column does not exist any more.
30
+ The "Adj Close" column is also missing from using ticker = yf.Ticker("AAPL") data = ticker.history(period="1y")
31
+
32
+ The fixes 1: In order to fix the issue in the function stock_prices_df, auto_adjust=False is used explicitly in download function, to get back the Adj Close column.
33
+
34
+ The fixes 2: The function "get_yearly_single_stock_data" in part 6 is broken duo to the missing "Adj Close" column
35
+ from ticker = yf.Ticker() and ticker.history(). Add auto_adjust=False into ticker.history(..., auto_adjust=False)
36
+ for fixing the issue. However, after the fix, the following line in the part 6 has an error:
37
+ complete_history = complete_history.merge(dld_history, how='left', left_index=True, right_index=True)
38
+ The root cause is that Columns of dld_history is of MultiIndex(, names=['Price', 'Ticker']). However, each price column
39
+ such as 'Close','AdjClose' has only single level with Ticker being column index name.
40
+ Dropping the column MultiIndex level ('Ticker') fixed the issue (dld_history.columns = dld_history.columns.droplevel(1) )
41
+
42
+ print("\n===== DataFrame Structure Information for debug =====")
43
+ print("Index Levels:", dld_history.index.names) # Shows the index levels
44
+ print("Index:", dld_history.index) # Shows the actual index
45
+ print("Columns:", dld_history.columns) # Shows column names
46
+ print("Data Types:\n", dld_history.dtypes) # Shows data types of each column
47
+ print("Shape (Rows, Columns):", dld_history.shape) # Shows the shape of the DataFrame
48
+ 2025-02.23.2000: Add the test cases for unit testing of part 1,2,3,4
49
+ Comment out part 5 which is not used, for better performance.
50
+ 2025-02.23.2001: temparary version for years_list = [1, 2, 3, 5, 10, 15, 20, 25, 30, 40, 50, 60]
51
+
52
  '''
53
+
54
+ script_version = 'version: (2025-02.23.2001)'
55
  import gradio as gr
56
  import yfinance as yf
57
  import pandas as pd
58
  import numpy as np
59
  from datetime import datetime, timedelta
60
  import pytz
61
+ DEBUG_ENABLED = True
62
  #==============================================================================
63
 
64
  print_yearly_total_return = True
65
+ num_years_calculation=52 # total years for calculation
66
 
67
  # Define a list of years to calculate the trailing returns, cumulative returns, and so on
68
  # remove the row of current year row since it is not a full year.
69
+ #years_list = [1, 2, 3, 5, 10, 15, 20, 25, 30, 40, 50, 60]
70
+ years_list = [1, 2, 3, 4, 5, 6, 7,8, 9,10, 11,12,13,14,15,16,17,18,19, 20, 25, 30, 40, 50, 60]
71
 
72
  # Set the stock tickers list
73
+ tickers_lists = [["qqq","hxq.to","spy", "vfv.to","xiu.to", "xbb.to","xcb.to","xhb.to"], #0 checking ETF
74
+ ["qqq","spy", "vfv.to", "vgg.to", "zlu.to", "xiu.to","zlb.to","vdy.to", "xfn.to", "ry.to", "td.to", "na.to",
75
+ "slf.to", "gwo.to", "bce.to", "t.to", "rci-b.to", "enb.to", "trp.to","cp.to"], #1 main monitoring list
76
+ ["xiu.to", "xfn.to", "na.to","ry.to", "bmo.to","bns.to", "td.to", "cm.to", "cwb.to",
77
+ "slf.to", "gwo.to", "bce.to", "t.to", "rci-b.to", "enb.to", "trp.to", "vdy.to","xdv.to","cdz.to","xdiv.to", "zeb.to"], #2 financial ETF & stocks
78
+ ["spy","qqq","tqqq","mags","msft","AAPL","goog","AMZN","NVDA","meta","tsla","BRK-A","shop.to","hxq.to"], #3 US mega stocks + risky shopfy
79
+ ["^DJI","dia","^GSPC","spy","voo","ivv", "tpu-u.to","vfv.to", "zsp.to","hxs.to","tpu.to","xus.to", "xsp.to",
80
+ "^IXIC","^ndx", "qqq","hxq.to","^GSPTSE","xic.to","xiu.to", "HXT.TO", "TTP.TO","ZCN.TO", "xfn.to", "xit.to"], #4 indexes and index ETFs
81
+ ["dia","^DJI","^GSPC","spy","vfv.to", "zsp.to","hxs.to","xus.to", "xsp.to",
82
+ "^IXIC","qqq","hxq.to","^GSPTSE","xic.to","xiu.to", "HXT.TO", "xfn.to"], #5 indexes and typical index ETFs
83
+ ["^IXIC","^ndx","ONEQ","CIBR","QQJG", "qqq", "tqqq", "spy", "vfv.to", "HXQ.to", "ZQQ.to", "XQQ.to", "QQC.to", "ZNQ.TO",
84
+ "xiu.to", "xit.to"], #6 Nasdaq ETF and TSX IT ETF
85
+ ["qqq","tqqq","sqqq", "QLD", "spy", "spxu", "upro", "sso", "spxl","tecl"], #7 leveraged ETFs
86
+ ["^IXIC","^DJI","^GSPC","^GSPTSE"], #8 testing
87
+ ["vfv.to","spy"] #9 testing
88
  ]
89
 
90
  #==============================================================================
91
+ # Part 1:
92
+ # retrieve daily adjusted close prices of a list of tickers from yahoo finance
93
+ # Generate the year-end adjusted close prices
94
+ # return year-end adjusted close prices, and daily adjusted close prices
95
+ def stock_prices_df(tickers_list, end_date_str):
96
+ tickers_list_upper = [ticker.upper() for ticker in tickers_list]
97
+ tickers_str = ", ".join(tickers_list_upper)
98
  try:
99
  '''
100
+ 'try' statement for handlingy the exception error for yf.download
 
 
 
101
  '''
102
+ # Download the historical data, see 2025-02.23.1655 revision note
103
+ data = yf.download(tickers_str, period="max", auto_adjust=False) # default changed to auto_adjust=True at yfinance version 0.2.54,
104
+ # when auto_adjust=True, Close = Adj Close and Adj Close does not exist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  except:
106
  return pd.DataFrame()
107
  else:
108
+ data_adj_close = data['Adj Close']
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ # Filter out rows with dates newer than calculation_end_date
111
+ data_adj_close = data_adj_close[data_adj_close.index <= end_date_str]
112
+ #print("\nDebug- stock_prices_df\n", data_adj_close)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
+ # Rearrange columns based on the order in tickers_list_upper
115
+ if len(tickers_list)>1:
116
+ data_adj_close = data_adj_close.reindex(columns=tickers_list_upper)
 
 
 
117
 
118
+ # needed this when having only a single ticker in the ticker list
119
+ if len(tickers_list_upper)==1:
120
+ data_adj_close = pd.DataFrame(data_adj_close)
121
+ data_adj_close.rename(columns={'Adj Close': tickers_list_upper[0]}, inplace=True)
122
+
123
+ data_adj_close.columns = map(str.lower, data_adj_close.columns) # must after pd.DataFrame(data_adj_close)
124
+ # data_adj_close_year_end = data_adj_close.resample('A').ffill().round(2) # must before index changed to date
125
+ data_adj_close_year_end = data_adj_close.resample('YE').ffill().round(2) # must before index changed to date
126
+
127
+ data_adj_close.index=data_adj_close.index.date
128
+ data_adj_close_year_end.index=data_adj_close_year_end.index.date
129
+
130
+ last_date = data_adj_close_year_end.index[-1]
131
+ data_adj_close_year_end = data_adj_close_year_end.rename(index={last_date: end_date_str})
132
+ #print("\nstock_prices_df\n", end_date_str, "\n", data_adj_close_year_end)
133
+ return data_adj_close_year_end, data_adj_close
134
+
135
+ #==============================================================================
136
+ # Part 2: Calculate annual returns at year end, and at any given day (by calculation_end_date_str)
137
+ #
138
+ # annual return calculation can start at any given day
139
+ def get_annual_returns_anyday_df(daily_adj_close_df, calculation_end_date_str):
140
+
141
+ calculation_end_date=pd.to_datetime(calculation_end_date_str).tz_localize('America/New_York')
142
+
143
+ # Create a DataFrame with a complete date range
144
+ date_range = pd.date_range(start=daily_adj_close_df.index.min(), end=daily_adj_close_df.index.max(), freq='D')
145
 
146
+ complete_stock_history = pd.DataFrame(index=date_range)
147
+ # Merge the complete DataFrame with the original stock_history
148
+ complete_stock_history = complete_stock_history.merge(daily_adj_close_df, how='left', left_index=True, right_index=True)
149
+ complete_stock_history = complete_stock_history.ffill() # fill the newy added rows with previous day value
150
+ '''
151
+ Filter out the rows that matches the month and date of calculation_end_date, which are the ends of
152
+ annual periods from the calculation_end_date.
153
+ '''
154
+ # Filter out rows with dates newer than calculation_end_date
155
+ #filtered_stock_history = complete_stock_history[complete_stock_history.index <= calculation_end_date]
156
+ # note" daily_adj_close_df satisfys daily_adj_close_df.index <= calculation_end_date
157
+ filtered_stock_history = complete_stock_history
158
+ #print(filtered_stock_history)
159
+ target_month=filtered_stock_history.index.max().month
160
+ target_day=filtered_stock_history.index.max().day
161
+ #print("target_month", target_month, "target_day",target_day, "start_year", filtered_stock_history.index.max().year)
162
+ annual_returns = filtered_stock_history[(filtered_stock_history.index.month == target_month)
163
+ & (filtered_stock_history.index.day ==target_day)]
164
+ annual_returns_percent = annual_returns.pct_change().dropna(how='all')
165
 
166
+ annual_returns_df = pd.DataFrame(annual_returns_percent)
167
+ #print("\ndebug-annual_returns_df\n", annual_returns_df)
168
+ return annual_returns_df
169
+
170
+ # annual return calculation can start at year end
171
+ def get_annual_returns_year_end_df(data_adj_close_df, calculation_end_date_str):
172
+ annual_returns_percent = data_adj_close_df.pct_change().dropna(how='all')
173
+ return annual_returns_percent
174
 
175
  #==============================================================================
176
+ # Part 3: calculate the annualized trailing total return from the data generated in step 1 & display
177
  # Define a function to calculate the annualized trailing total return for a given number of years
178
  def get_trailing_return(ticker, data, years):
179
  # Get the total return values for the last n years
180
  trailing_data = data[ticker].tail(years)
181
  # Check if there are empty values within years
182
  if trailing_data.isna().any():
183
+ return np.nan
184
  # Check if there are valid total return values for all years
185
  if len(trailing_data) == years:
186
  # Convert the percentage strings to numeric values
 
195
  annualized_trailing_return = annualized_trailing_return.round(2)
196
  return annualized_trailing_return
197
  else:
198
+ return np.nan
199
 
200
  # Define a function to Loop through the list and print the trailing returns for each num_years
201
  def get_trailing_return_column(ticker, annual_returns_df):
 
209
  trailing_return_column[f"{num_years}-Year"] = trailing_return
210
  else:
211
  print(f"Data not available for {ticker}. Skipping.")
212
+ trailing_return_column[f"{num_years}-Year"] = np.nan
213
  return trailing_return_column
214
 
215
  # Create an empty DataFrame to store all tickers' trailing returns
216
+ def get_trailing_return_all(annual_returns_df):
217
  all_tickers_trailing_returns_df = pd.DataFrame(index=years_list)
218
+ tickers=annual_returns_df.columns.tolist()
219
  # Loop through each ticker in the list
220
  for ticker in tickers:
221
  trailing_returns = get_trailing_return_column(ticker, annual_returns_df)
 
224
  return all_tickers_trailing_returns_df
225
 
226
  #==============================================================================
227
+ # Part 4: calculate the cumulative return from the data (all_tickers_returns_df) generated in part 1 & display
228
  # Define a function to calculate the cumulative return for a given number of years from a ticker
229
  def get_cumulative_return(ticker, data, years):
230
  # Calculate the cumulative return
 
241
  cumulative_returns[years] = cumulative_return.iloc[-1]
242
  return cumulative_returns
243
 
244
+ def get_cumulative_return_all(annual_returns_df):
245
  # Create an empty DataFrame with years_list as the index for cumulative returns
246
  all_tickers_cumulative_returns_df = pd.DataFrame(index=years_list)
247
+ tickers=annual_returns_df.columns.tolist()
248
  # Loop through each ticker in the list
249
  for ticker in tickers:
250
  cumulative_returns = get_cumulative_return_column(ticker, annual_returns_df)
 
253
  return all_tickers_cumulative_returns_df
254
 
255
  #==============================================================================
256
+ # Part 5: calculate the CAGR (Compound Annual Growth Rate) from the data
257
  # in all_tickers_cumulative_returns_df generated earlier & display
258
  # Define a function to calculate the CAGR from the cumulative value and the years
259
  def calculate_cagr(value, years):
 
275
  all_tickers_cagrs_df = all_tickers_cumulative_returns_df.apply(lambda x: calculate_cagr(x, x.index), axis=0)
276
  return all_tickers_cagrs_df
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  #==============================================================================
279
  # Part 6:
280
  # single ticker's Prices, Returns,Dividends, good for verifying whether "Adj Close" is correct.
 
288
  def get_yearly_single_stock_data(ticker):
289
  stock = yf.Ticker(ticker)
290
  #-------- mainly for downloading 'Dividends'
291
+ history = stock.history(period="max", auto_adjust=False) # see 2025-02.23.1655 revision note
292
  dividend_history=history['Dividends']
293
  dividend_history.index=dividend_history.index.date
294
 
295
  #-------- mainly for downloading 'Close','Adj Close'
296
+ dld_history=yf.download(ticker, period="max", auto_adjust=False) # see 2025-02.23.1655 revision note
297
  dld_history=dld_history[['Close','Adj Close']]
298
  dld_history.rename(columns={'Adj Close': 'AdjClose'}, inplace=True)
299
+ '''
300
+ note: see 2025-02.23.1655 revision note
301
+ Columns is of MultiIndex(, names=['Price', 'Ticker']). Each price colums such as 'Close','AdjClose'
302
+ has only single sub-column with Ticker is column index name.
303
+ Drop the column MultiIndex level ('Ticker')
304
+ '''
305
+ dld_history.columns = dld_history.columns.droplevel(1) # see 2025-02.23.1655 revision note
306
  date_range = pd.date_range(start=dld_history.index.min(), end=dld_history.index.max(), freq='D')
307
  complete_history = pd.DataFrame(index=date_range)
308
 
 
346
  return yearly_data
347
 
348
  #==============================================================================
349
+ # Part 7: utility functions
350
+ # get the last trading day of S&P 500 in string format
351
+ def get_last_trading_day():
352
+ # Get today's date, use .strftime("%Y-%m-%d") to convert to a string
353
+ today_date_str=datetime.now(pytz.timezone('America/New_York')).date().strftime("%Y-%m-%d")
354
+ stock = yf.Ticker("^GSPC") # S&P 500 (^GSPC) ticker
355
+ # search and see yfinance_BUG_1 NOTE in this file
356
+ history_df=stock.history(period="max", end=today_date_str)["Close"]
357
+ last_trading_day_str = history_df.index.max().date().strftime("%Y-%m-%d")
358
+ return last_trading_day_str
359
+
360
+ def str_to_integer(integer_str):
361
+ try:
362
+ integer_number = int(integer_str)
363
+ return integer_number
364
+ except ValueError:
365
+ return -1
366
+
367
+ # validate the date string
368
+ def is_valid_date(date_string):
369
+ try:
370
+ # Attempt to parse the date string
371
+ datetime.strptime(date_string, "%Y-%m-%d")
372
+ return True
373
+ except ValueError:
374
+ # Raised when the date string is not in the expected format
375
+ return False
376
+
377
+ def date_label_conversion_strip_time(all_tickers_returns_df, calculation_end_date_str):
378
+ all_tickers_returns_df.index=all_tickers_returns_df.index.date
379
+ all_tickers_returns_df.index.name='date'
380
+ # print("debug get_annual_returns_tickers_df", all_tickers_returns_df)
381
+ # Convert calculation_end_date_str to a datetime object, replace the index's mon/day portion of date
382
+ end_date_datetime_obj = datetime.strptime(calculation_end_date_str, "%Y-%m-%d")
383
+ all_tickers_returns_df.index = all_tickers_returns_df.index.map(
384
+ lambda x: x.replace(month=end_date_datetime_obj.month,
385
+ day=end_date_datetime_obj.day))
386
+ return all_tickers_returns_df
387
+
388
+ #==============================================================================
389
+ # Part 8: gradio handling - Input command handling and display in web page
390
 
391
  help_info_str="Input Formats:\n \
392
  1. ticker list....................Example: spy vfv.to xiu.to xic.to xfn.to ry.to \n \
393
+ 2. One of default ticker list, a number between 1 and 7....Example: 0, or 1, ...,7 \n \
394
  3. CalculationEndDate as prefix. Example: 2020-12-31 2 \n \
395
  .........................................2020-12-31 spy vfv.to xiu.to xic.to xfn.to ry.to \n \
396
  4. single ticker: Dividend/Close/AdjClose/Return/TotalReturn/CalReturn(by Close/Dividends). @1 spy \n \
397
+ note: daily adjusted close data are from Yahoo Finance. \n" + script_version
398
 
399
+ # Main Handling Process
400
+ def calculation_response(message):
401
  # if there is no input, display help information
402
  if message=="":
403
  return help_info_str
 
447
  # calculation_end_date_for_others are for trailing and cumulative returns
448
  calculation_end_date_for_others_str=calculation_end_date_month_boundary_date_str
449
 
450
+ ''' Handling Feb 29 of leap years.
451
+ For leap years, to simiplify the calculation, Feb 28 will be used to replace Feb 29 for
452
+ for calculating returns.
453
+ Therefore, if calculation_end_date_for_others_str is Feb 29, then replace 29 to 28 of calculation_end_date_for_others_str
454
  '''
455
+ leap_year=False
456
+ if (
457
+ calculation_end_date_for_others_str[-5:] == '02-29'
458
+ ):
459
+ calculation_end_date_for_others_str = calculation_end_date_for_others_str[:-2] + '28'
460
+ leap_year=True
461
  #................End
462
 
463
  # Check whether numebr 0, 1, 2, .. is selected for using a default ticker list
 
468
  # if no tickers were set, display help information
469
  if len(tickers)==0:
470
  return help_info_str
471
+ tmp_ticker_list=tickers
472
+ tickers = [ticker.lower() for ticker in tmp_ticker_list]
473
 
474
  #*********************************************************************************
475
+ # Calculating year-end prices, Annual, Trailing, Cumulative, and CAGR returns & generating html for display
476
+ #
477
+ # list of year-end prices of stocks
478
+ output_string1= f"\nAdj Close Prices ($) at year-end\n"
479
+ data_adj_close_year_end_df, data_adj_close_df = stock_prices_df(tickers, calculation_end_date_str)
480
+ output_dataframe= data_adj_close_year_end_df
481
+ output_html1=output_string1 + output_dataframe.to_html()
482
+ #print("\ndebug1 output_dataframe\n", output_string1, output_dataframe)
483
+
484
+ # Annual Total Return
485
  output_string = f"\nAnnual Total Return (%) as {calculation_end_date_str}\n"
486
+ #output_dataframe = get_annual_returns_tickers_year_boundary_df(tickers, calculation_end_date_str)
487
+ output_dataframe = get_annual_returns_year_end_df(data_adj_close_year_end_df, calculation_end_date_str)
488
+ output_dataframe = output_dataframe.dropna(how='all')
489
  output_dataframe = output_dataframe.round(4)*100
490
+ #output_dataframe.index=output_dataframe.index.date
491
  # Assuming your DataFrame is named output_dataframe
492
  last_date = output_dataframe.index[-1]
493
  output_dataframe = output_dataframe.rename(index={last_date: calculation_end_date_str})
494
  # Convert the DataFrame to HTML, Combine the expected string outputs
495
+ output_html2 = output_string + output_dataframe.to_html()
496
+ #print("\ndebug2 output_dataframe\n", output_dataframe)
497
 
498
  # annual_returns - at any given day, for calculating trailing and cumulative returns, not to be displayed
499
+ #annual_returns_dataframe=get_annual_returns_tickers_df(tickers, calculation_end_date_for_others_str)
500
+ annual_returns_dataframe=get_annual_returns_anyday_df(data_adj_close_df, calculation_end_date_str)
501
+ #print("\ndebug2-T annual_returns_dataframe\n", annual_returns_dataframe)
502
 
503
  # Trailing Return
504
+ if (leap_year):
505
+ output_string3 = f"\nTrailing Total Return (%) as {calculation_end_date_for_others_str} (leap year: Feb 29 replaced by Feb 28 for approximation)\n"
506
+ else:
507
+ output_string3 = f"\nTrailing Total Return (%) as {calculation_end_date_for_others_str}\n"
508
+ output_dataframe3=get_trailing_return_all(annual_returns_dataframe)
509
+ output_dataframe3 = output_dataframe3.dropna(how='all')
510
  # Insert an empty to align the ticker symbols with annual return display
511
+ output_dataframe3.insert(0, "-", " ")
512
+ output_dataframe3.index.name="yrs"
 
 
 
 
 
 
 
513
  output_html3=output_string3 + output_dataframe3.to_html()
514
+ #print("\ndebug3\n", output_string3, output_dataframe3)
515
+ # Cumulative Return
516
+ output_string4 = f"\nCumulative Return (%) as {calculation_end_date_for_others_str}\n"
517
+ cumulative_return_all_dataframe=get_cumulative_return_all(annual_returns_dataframe)
518
+ cumulative_return_all_dataframe = cumulative_return_all_dataframe.dropna(how='all')
519
+ output_dataframe4=cumulative_return_all_dataframe.round(4)*100
520
+ output_dataframe4.index.name="yrs"
521
  output_html4=output_string4 + output_dataframe4.to_html()
522
 
523
+ # CAGR Return
524
+ '''
525
+ # following code is fine, but is not needed
526
+ output_string5 = f"\nCompound Annual Growth Rate (CAGR) (%) as {calculation_end_date_for_others_str}\n"
527
+ output_dataframe5=get_cagr_return_all (cumulative_return_all_dataframe)
528
+ output_dataframe5=output_dataframe5.round(4)*100
529
+ output_html5=output_string5 + output_dataframe5.to_html()
530
+ '''
531
+ # print total 1,2,3,4 (not 5)
532
+ output_html = output_html1 + output_html2 + output_html3 + output_html4
533
  return output_html
534
 
535
+ # Gradio Web interface
536
+ with gr.Blocks() as web_block:
537
+
538
+ chatbot = gr.Chatbot(height="500px")
539
+ # Create a row element for the Textbox and Clear button
540
+ with gr.Row():
541
+ #msg = gr.Textbox(label="stock tickers input", scale=2, min_width=380)
542
+ msg = gr.Textbox(show_label=False, scale=2, min_width=380)
543
+ clear = gr.ClearButton([msg, chatbot], scale=0, min_width=50)
544
+
545
+ def respond(message, chat_history):
546
+ bot_message = calculation_response(message)
547
+ chat_history.append((message, bot_message))
548
+ return "", chat_history
549
+
550
+ msg.submit(respond, # function
551
+ [msg, chatbot], # inputs of the function
552
+ [msg, chatbot] # outputs of the function
553
+ )
554
+ web_block.launch()
555
+ #web_block.launch(debug=True)
556
+
557
+ #----------- test cases-----------------
558
+ #-------- part 1 stock_prices_df
559
+ calculation_end_date_str="2025-02-21"
560
+ data_adj_close_year_end_df, data_adj_close_df = stock_prices_df(["SPY", "MSFT"], "2025-02-21")
561
+ #print("\ndebug data_adj_close_df data_adj_close_year_end_df\n", data_adj_close_year_end_df, "\ndata_adj_close_df\n",data_adj_close_df)
562
+
563
+ #tickers = yf.download(["AAPL", "MSFT"], period="1y", auto_adjust=False) # default changed to auto_adjust=True at yfinance version 0.2.54
564
+ # when auto_adjust=True, Close = Adj Close and Adj Close does not exist
565
+ #print("\ndebug test2\n", tickers)
566
+ #tickers = yf.download(["AAPL", "MSFT"], period="1y")
567
+ #print("\ndebug test\n", tickers)
568
+
569
+ #-------- part 2 get_annual_returns_year_end_df
570
+ output_dataframe = get_annual_returns_year_end_df(data_adj_close_year_end_df, calculation_end_date_str)
571
+ #print("\ndebug get_annual_returns_year_end_df\n", output_dataframe)
572
+
573
+ # for calculating trailing return
574
+ annual_returns_dataframe=get_annual_returns_anyday_df(data_adj_close_df, calculation_end_date_str)
575
+ #print("\ndebug get_annual_returns_anyday_df\n", annual_returns_dataframe)
576
+
577
+ #-------- part 3 get_trailing_return_all
578
+ output_dataframe3=get_trailing_return_all(annual_returns_dataframe)
579
+ #print("\ndebug get_trailing_return_all\n", output_dataframe3)
580
+
581
+ #-------- part 4 get_cumulative_return_all
582
+ cumulative_return_all_dataframe=get_cumulative_return_all(annual_returns_dataframe)
583
+ #print("\ndebug get_cumulative_return_all\n", cumulative_return_all_dataframe)
584
+
585
+ #-------- part 5 get_cagr_return_all
586
+ #output_dataframe5=get_cagr_return_all (cumulative_return_all_dataframe)
587
+ #print("\ndebug get_cagr_return_all\n", output_dataframe5)
588
+
589
+ #-------- part 6 stock_prices_df
590
+ #output_dataframe0=get_yearly_single_stock_data("SPY")
591
+ #print("\ndebug part 6 test\n", output_dataframe0)
592
+
593
+ #-------- testing calculation_response
594
+ #calculation_response("8")
595
+ #bot_message = calculation_response("SPY")
596
+ #print(bot_message)
597
+