להתאמץ או לא להתאמץ, זו השאלה Uval Vered, מרץ 14, 2024מרץ 29, 2024 חשבתם פעם איך נכון לקנות מדד? אמנם לא שאלה סקסית במיוחד, אבל הרבה זמן מעסיקה אותי. בתור מי שקונה מדדים באופן קבוע פעם בחודש, מה הדרך הנכונה לעשות את זה? האם פשוט לבחור תאריך אקראי ולקנות כל חודש באותו תאריך (בהינתן שיש מסחר בתאריך הזה), או לחליפין דווקא איזו שיטה חמדנית כזו, למשל להיות חרוץ ולקנות רק כשיש ירידה מסוימת ואם לא הייתה אז נקנה בסוף החודש.קיצר, החלטתי לבדוק. התוצאה דיי הפתיע אותי, מודה.בפוסט הקרוב אני אציג את מתודולוגיית הבדיקה, התוצאות והקוד שאיתו עשיתי את זה. יהיהו כאן ממוצעים גיאומטריים, פנדות ותשואות. תעלו לסיפון, יוצאים. מתודולוגיה נבחן כאמור שתי שיטות לקנות מדד. המדד הקורבן (איך לא) יהיה הS&P500. אבל הקוד ורסטילי, בשינוי יחיד אפשר לבדוק כל מדד אחר.בשתי השיטות, בכל חודש אני קונה את המדד ב500$ (גם כן נתון לשינוי) ומחזיק. הבדיקה נעשית בין 01.02.2004 ל- 01.02.2024. כלומר על פני 20 שנה. קונים ירידות בגישה הראשונה נרצה לקנות את המדד רק כאשר יש בחודש ירידה מסוימת בסיום יום המסחר. כלומר ההבדל בין סגירת המסחר של יום קודם לזו של היום הסתכמה בירידה של לפחות X אחוז. במידה ואכן הייתה הירידה, נקנה בשער הסגירה של הירידה. במידה ובכל החודש לא הייתה ירידה של X אחוז נקנה ביום המסחר האחרון לחודש. במידה והייתה יותר מירידה אחת של X אחוז או יותר, נקנה במופע הראשון שלה. מוצאים את X: נרצה למצוא X כזה שישנם מספיק חודשים שמבהלך המסחר אכן תיהיה ירידה בX אחוז. כדי להשאיר את הדיון קליל ומובן, X יהיה ערך דיסקרטי. כלומר לא חיפשתי ערך אופטימלי על כל הרצף ש-X יכול להיות אלא הסתכלתי על קפיצות של רבעי אחוזים. גיליתי שמספר הזהב כאן הוא X=-1%. מינוס אחד אחוז מניב את התיק הגדול ביותר מכל רבעי האחוזים שבין 2.5- ל0, ו- 76.2% מהחודשים חווים ירידה של לפחות אחוז אחד במהלך חודש המסחר בסגירה בין שני ימים. בפחות מ-50% מהחודשים (למעל מתחת לX=-1.5%) התהליך פשוט ירגיש מיותר מכיוון שברוב החודשים ניאלץ לקנות במחיר סוף החודש, ולכן חיפשתי שכמות המופעים תיהיה מעל חצי ותניב גודל תיק אופטימלי. הערה – מאוד סביר שאם לא הייתי מגביל את זה שיום המסחר ממש יסגר בירידה של לפחות X אחוז היינו מוצאים יותר מופעים בתוך ימי המסחר, כלומר ימי שבמהלך היום הייתה ירידה של X אחוז אבל היום לא נסגר כך אלא עלה בשלב כלשהו במסחר. אבל אז לא היה סביר שהמשקיע הממוצע ישב כל היום ויארוב לירידה כזו ולכן הכרחתי שיום המסחר יסגר כך. הערה 2 – שימו לב שיותר משלושה רבעים מהחודשים (!) חווים ירידה של לפחות אחד אחוז בסגירת המסחר במהלך החודש. זה גם מלמד אותנו על הצורך לא להבהל מירידות וככל שנפתח יותר פעמים את התיק נחווה יותר ירידות. הרחבה על זה כאן. התוצאה: כאמור קניה של 500$ בכל חודש, ב20 השנה האחרונות ובירידה של לפחות 1% או בסוף החודש הניבה 339,146$ (ללא התחשבות בדיבידנדים).ראשית שימו לב שאם סתם היינו שמים בצד 500$ בחודש במשך כל 240 החודשים היינו צוברים 120,000$. כלומר הכסף כמעט שילש את עצמו.שנית, לפני שנשווה לתוצאות השיטה השניה, יש כאן עוד הרבה ניתוחי המשך אפשריים. הסקריפט שלי בודק כמה מופעים גם יש בכל חודש, אבל יש מקום לבדוק כמה פעמים אכן קנינו בירידה גדול מ1%, מה היה ממוצע הירידות שקנינו בהם. האם ישנם מספיק חודשים שבהם יותר ממופע אחד כך שאולי נשקול לקנות דווקא במופע שני/ שלישי וכו. כיוון שזו לא מטרת הניתוח הזה עצרתי כאן. בוחרים תאריך אקראי וקונים שיטה שניה היא דיי משעממת. פשוט לבחור תאריך אקראי ולקנות. אבל איזה תאריך? ומה אם נפלנו על תאריך שאין בו מסחר? ומה אם השנה היא שנה מעוברת ויש 29 לפברואר?אז כדי שלא ניהיה מושפעים מניסבתיות מוזרה, כמו למשל שה-6 לחודש יתגלה כאיזה תאריך זהב, הרצתי את הבדיקה על כל התאריכים האפשריים. כלומר אם כל 20 השנה האלה היינו קונים בראשון לחודש, בשני לחודש וכן הלאה. ממוצע על כל גדלי התיקים שהתקבלו זה גודל התיק המצופה מבחירת יום אקראי.אם אין מסחר ביום שבחרנו בחודש מסוים, זה יקנה את ערך הסגירה של יום המסחר הקודם. אם היום עצמו לא קיים באותו חודש, למשל ה31 לחודש, נקנה ביום האחרון לאותו חודש (30, 29 או 28).הערה – שימו לב ל4 הנקודות הימניות בגרף. ארבעת הימים האחרונים בחודש הם כביכול החלשים ביותר לקניה. ה29 כמובן על פי רוב לא קיים, ה30 וה-31 מתקיימים לסירוגין. לכן בעצם המסקנה המתבקשת היא שהיומיים האחרונים לחודש הם חלשים יותר ואלו לא 4 נקודות אלא שתיים, אך עם זאת כדי לשים את האצבע על מה בדיוק קורה שם צריך לבצע ניתוח בנפרד.התוצאה: גודל התיק הממוצע הוא 388,871$, כאשר סטיית התקן בין הימים היא 417$. הבדל של 275$ בין השיטות. מסקנה אם אתם רוצים להתאמץ ולקבל תשואה עודפת על ידי קניה בירידות החודשיות ולא בתאריך רנדומלי, יש לי דבר אחד לומר לכם קוד אפשר לשפור את הסיבוכיות של הקוד כמובן. השתדלתי שיהיה פשוט ככל הניתן. מומלץ להשתמש בJupyter notebook. שימו לב שבהרצה ראשונה בג’ופיטר פונקציות השירות צריכות לבוא לפני הקוד העיקרי. import yfinance as yf import pandas as pd import calendar from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta import numpy as np import matplotlib.pyplot as plt last_trade_day_date = previous_date(END_DATE) # In sp500 exists only trading days sp500 = yf.download('^GSPC', start=START_DATE, end=END_DATE) # In sp500_filled all days exists. non trading days gets filled with previous trading day values sp500_filled = sp500.resample('D').ffill() #The S&P500 closing index level for the time frame closing_value = sp500_filled.loc[last_trade_day_date]['Adj Close'] last_trading_days = sp500.resample('M', convention='end').last() daily_returns = sp500['Adj Close'].pct_change() daily_returns = daily_returns*100 method_results = invest_based_on_decline_of_x(-1,500,START_DATE, last_trade_day_date) results_df = pd.DataFrame.from_dict(method_results, orient='index') num_months_with_decline = results_df[results_df['#_occurrences'] > 0].shape[0] total_num_months = results_df.shape[0] print(f'total num of months is {total_num_months}, percent of qalified months is {num_months_with_decline/total_num_months}') total_investment = results_df['investment_total_result'].sum() print(f'total investment result is {total_investment}') investments_results = [] for day in range(1,32): total_investment = calculate_monthly_investment_for_given_date(day, 500, START_DATE, last_trade_day_date) investments_results.append(total_investment) print(investments_results) # Calculate average, variance, and standard deviation average = np.mean(investments_results) variance = np.var(investments_results) std_deviation = np.std(investments_results) print("Average:", average) print("Variance:", variance) print("Standard Deviation:", std_deviation) # Plot the results plt.figure(figsize=(10, 6)) plt.scatter(range(1, len(investments_results) + 1), investments_results, label='Investments Results') plt.axhline(average, color='red', linestyle='--', label='Average') plt.fill_between(range(len(investments_results)), average - std_deviation, average + std_deviation, color='gray', alpha=0.2, label='Standard Deviation') plt.legend() plt.title('Investments Results Analysis') plt.xlabel('Day Of Investment') plt.ylabel('Investment Outcome') plt.grid(True) plt.show() פונקציות שירות def previous_date(current_date): """ The END_DATE is not included in the downloaded data, thus grabbing previous day """ current_date = datetime.strptime(current_date, "%Y-%m-%d") previous_date = current_date - timedelta(days=1) return previous_date def is_leap_year(year): # at a leap year February gets 29th, otherwise 28th last day of Feb if year % 4 == 0: if year % 100 == 0: if year % 400 == 0: return True else: return False else: return True else: return False def last_day_of_month(month, year): """ Find the last day of the given month and year, taking into account leap years. """ if month == 2: if is_leap_year(year): return 29 else: return 28 elif month in [4, 6, 9, 11]: return 30 else: return 31 פונקציות עיקריות def month_without_decline_check(current_date, results_dict, monthly_amount, monthly_str): """ fills dict values for months without x declines """ if current_date.day == last_day_of_month(current_date.month, current_date.year) and monthly_str not in results_dict: #its last day of the month and there were no declines this month so its not in the dict already last_trade_day_val = last_trading_days.loc[monthly_str]['Adj Close'][0] #the index value for last trading day of the month results_dict[monthly_str] = {'#_occurrences' : 0, 'first_occurrence' : 'N/A','sp_value' : last_trade_day_val,'investment_total_result': closing_value/last_trade_day_val*monthly_amount} def invest_based_on_decline_of_x(x, monthly_amount, start_date, end_date): """ For each month, finding out how many days of at least -X% change was, making an investment that day and calculate its compounded value Documenting what was the first day of -x percent decline or more (if any). buyig on the first -x decline """ results = {} month_cnt = 0 current_date = datetime.strptime(start_date, '%Y-%m-%d') current_month = current_date.month while current_date <= end_date: monthly_str = current_date.strftime('%Y-%m') daily_str = current_date.strftime('%Y-%m-%d') try: closing_change = daily_returns.loc[daily_str] except KeyError: # print(f'The was a key error for date {current_date}') month_without_decline_check(current_date, results, monthly_amount, monthly_str) current_date += timedelta(days=1) # moving one day forward continue if closing_change <= x: if monthly_str in results: results[monthly_str]['#_occurrences'] += 1 else: #first occurance for the month results[monthly_str] = {'#_occurrences' : 1, 'first_occurrence' : daily_str, 'sp_value' : sp500.loc[daily_str]['Adj Close'], 'investment_total_result': closing_value/sp500.loc[daily_str]['Adj Close']*monthly_amount} month_without_decline_check(current_date, results, monthly_amount, monthly_str) current_date += timedelta(days=1) # moving one day forward return results def calculate_monthly_investment_for_given_date(day, monthly_amount, start_date, end_date): """ calculate investment for a given day in the month. if this is not a trading day than taking the previous trading day closing price if such day doesn't exist (i.e. 30th of February) taking last day of the month """ total_investment = 0 current_date = datetime.strptime(start_date, '%Y-%m-%d') while current_date <= end_date: last_day_of_the_month = last_day_of_month(current_date.month, current_date.year) if last_day_of_the_month < day: day = last_day_of_the_month monthly_str = current_date.strftime('%Y-%m') daily_str = monthly_str + '-' + str(day) try: investment_result = closing_value/sp500_filled.loc[daily_str]['Adj Close']*monthly_amount except KeyError: print(f'There was a key error for date {daily_str}') # the day does not exist in data set. typically the START_DAY itself daily_str = monthly_str + '-' + str(day+1) investment_result = closing_value/sp500_filled.loc[daily_str]['Adj Close']*monthly_amount total_investment += investment_result current_date += relativedelta(months=1) # moving one month forward return total_investment statistics
היי אילן, מתנצל שאני חוזר אלייך כל כך מאוחר – תקופה קצת משוגעת. בשמחה אבצע את הבדיקה שאתה שואל לגביה, אבל אני צריך הגדרה יותר מדויקת. אתה רוצה כל שנה לעשות ריבאלנס ככה שתיק הוא 50% מניות ומה החצי השני? האם את הריבאלנס אתה רוצה לעשות על ידי מכירה של מניות או קניה של הנכס השני בתיק (למשל אג”חים)? בנוסף האם לאורך השנה מכניסים X כסף כל חודש שמתחלק שווה בשווה בין שני סוגי הנכסים?
תודה על ההשקעה! יהיה מעניין לבדוק באותה השיטה, מה היה קורה לכסף אם היית נכנס בשנים אחרות. כלומר-מה היה קורה אם היית נכנס בדיוק בשיאו של גל ירידות? או בסופו? שהרי כשאתה מתחיל להשקיע לא ידוע באיזה שלב של מחזוריות השוק אתה נמצא.
היי ניר, אם תרצה לציין מחזור שנים ספציפיות אוכל להחזיר לך תשובה בקלות. מנגד, אני חושב שזה קצת מוציא תא העוקת להנדס את התשובה שאנחנו נכנסים בה שהרי אנחנו יודעים לומר באיזה חלק של הסייקל אנחנו (ובכלל גם איזה מן סייקל זה) רק בסופו.