Exploring Different Market Phenomenons
Backtesting a Simple Investment Strategy
By Chi Kit Yeung in Python Investing
August 18, 2024
Introduction
I’ve been thinking more about personal finance and stock market investing lately. It probably started back in 2022 during the GameStop shorting shenanigans conjured up by the wallstreetbets subreddit. After many bad investment choices since then (it’s gambling innit), I’ve learned that the best strategy is to dollar cost average (DCA) into a broad market index fund and ride out any market volatility and turbulence that will inevitably occur. Each month I will put aside some money and put it into some market tracking index fund regardless of how the market is performing at the time. In other words, don’t try to time the market. Time in the market beats timing the market.
Having said that, have you heard of the ‘Day of Week’ phenomenon? How about the the ‘Turn-of-the-Month Effect’? The ‘Day of Week’ phenomenon describes a regularly occuring observation that market returns on Fridays tend to be higher than on Monday1. On top of that, the ‘Turn-of-the-Month Effect’ describes the observation that stock prices tend to rise on the final trading day of the month and the first three trading days of the following month. I won’t be debating how or why it happens, or even whether these phenomena are real, there are many scientific outlets and research out there participating in that argument. I just want to make money.
The Idea
I know timing the market is basically impossible. Especially as someone with a terrible track record in investing as myself. But what if I can take advantage of these two market ‘Calendar Effects’2, as they are known, to supplement my ongoing DCA strategy? Theoretically, if I avoid buying stocks on the last day of the month and on Fridays, I avoid spending money on days where stocks are statistically known to be more expensive.
Backtesting
Before spending real money on this idea, let’s first test it out with historical prices using code.
Preparing the Data
Data
I’ll be using data for Vanguard S&P 500 ETF (VOO) and CSOP Hang Seng Index Daily (2x) Leveraged Product (7200.HK) which tracks the US’ S&P 500 index and Hong Kong’s Hang Seng index, respectively. The data was downloaded from Yahoo Finance34.
VOO’s dataset contains data starting from September 9, 2010 until August 16, 2024. 7200.HK’s dataset contains data starting from March 14, 2017 until August 16, 2024.
import yfinance
voo_dat = yf.download('VOO').reset_index()
hsi_dat = yf.download('7200.HK').reset_index()
>>> voo_dat.describe()
Date Open High Low Close Adj Close Volume
count 3508 3508.000000 3508.000000 3508.000000 3508.000000 3508.000000 3.508000e+03
mean 2017-08-27 10:23:56.716077568 249.883743 251.169732 248.473555 249.921143 228.703332 2.683051e+06
min 2010-09-09 00:00:00 99.139999 101.860001 98.239998 100.339996 78.213005 8.600000e+03
25% 2014-03-05 18:00:00 169.180000 170.037495 168.410000 169.289997 140.771465 1.020750e+06
50% 2017-08-26 12:00:00 226.355004 227.144996 225.159996 226.464996 201.084412 2.216950e+06
75% 2021-02-22 06:00:00 345.457512 348.552506 342.024994 345.567497 331.466431 3.649375e+06
max 2024-08-16 00:00:00 517.330017 519.400024 516.599976 519.039978 519.039978 2.466930e+07
std NaN 106.090662 106.697582 105.457683 106.115028 112.621653 2.413119e+06
Cleaning
Since I’m interested in the day of the week, I’ll be adding a new column weekday
to the dataframe as follows.
voo_dat['weekday'] = [i.weekday() for i in voo_dat['Date']]
hsi_dat['weekday'] = [i.weekday() for i in hsi_dat['Date']]
>>> voo_dat.head()
Date Open High Low Close Adj Close Volume weekday
0 2010-09-09 102.500000 102.500000 101.139999 101.320000 78.213005 26500 3
1 2010-09-10 101.680000 101.860001 101.300003 101.779999 78.568146 8600 4
2 2010-09-13 102.959999 103.139999 102.500000 103.059998 79.556206 33750 0
3 2010-09-14 102.839996 103.480003 102.379997 103.040001 79.540764 59400 1
4 2010-09-15 102.620003 103.379997 102.400002 103.300003 79.741478 9250 2
As you can see, the appended weekday is represented as an integer. Each number represents the day of the week starting with 0 = Monday, 1 = Tuesday, 4 = Friday, etc. all the way to 6 = Sunday. But since our data only contains the ticker prices on trading days, we can expect only values ranging from 0-4 in the dataset since the markets are closed on weekends.
Next, I’ll also be removing some of the columns that won’t be used.
voo = voo_dat.drop(columns=['High', 'Low', 'Adj Close', 'Volume'])
hsi = hsi_dat.drop(columns=['High', 'Low', 'Adj Close', 'Volume'])
Done.
Day of Week Strategy
TLDR skip to Results
As mentioned earlier, the ‘Day of Week’ phenomenon refers to where market returns on Fridays tend to be higher than on Mondays. I’ve written a simple script that would simulate buying as much stock as possible on Monday based on a predetermined starting budget and then selling everything on Friday. The function would generate a dataset of all the transactions it made so that it can be analyzed further.
The Strategy
# Monday 0
# Wednesday 2
# Friday 4
def strategy(data:pd.DataFrame, buy:int = 0, sell:int = 4, hold = 0, date=None) -> pd.DataFrame:
"""
DEFAULT: Buy on Monday, Sell on Friday
data: OHLC data of the security
buy: Day of the week to buy
sell: Day of the week to sell
hold: Number of weeks to hold asset before selling
date: Starting date of the strategy. Default `None` will simulate earliest available data.
"""
if date is None:
data_filtered = data.copy()
else:
data_filtered = data[data['Date'] >= date]
trade_book = pd.DataFrame(columns=['date', 'trade', 'qty', 'price', 'balance', 'holdings'])
balance: int = 10000 # Starting amound in $
holding: int = 0
hold_length: int = 0
for index, row in data_filtered.iterrows():
day = row['weekday']
closing_price = row['Close']
if day == buy and hold_length == 0:
qty = math.floor(balance/closing_price)
balance -= qty * closing_price
holding += qty
trade_book.loc[len(trade_book)] = [row['Date'], 'buy', qty, closing_price, balance, holding]
hold_length += hold
elif day == buy and hold_length > 0:
hold_length -= 1
elif day == sell and hold_length == 0:
qty = holding
balance += holding * closing_price
holding -= holding
trade_book.loc[len(trade_book)] = [row['Date'], 'sell', qty, closing_price, balance, holding]
return trade_book
Results
Let’s try running the code on VOO.
First up, buying on Mondays and selling everything on Friday. With a starting budget of $10,000 and starting on September 9, 2010, today the strategy would net out a balance of $43,172.95. A 431.73% gain.
>>> strategy(voo).tail()
date trade qty price balance holdings
1355 2024-08-02 sell 82 489.910004 40316.952031 0
1356 2024-08-05 buy 84 475.200012 400.151023 84
1357 2024-08-09 sell 84 489.820007 41545.031611 0
1358 2024-08-12 buy 84 490.070007 379.151023 84
1359 2024-08-16 sell 84 509.450012 43172.952031 0
Next, buying on Mondays, holding it for a week, and then selling everything come Friday. Last Friday, the strategy would net out a balance of $44,542.39. A 445.42% gain. Appreciably higher than the previous strategy.
>>> strategy(voo, hold=1).tail()
date trade qty price balance holdings
676 2024-07-15 buy 90 516.109985 491.88906 90
677 2024-07-26 sell 90 500.329987 45521.58789 0
678 2024-07-29 buy 90 500.700012 458.58681 90
679 2024-08-09 sell 90 489.820007 44542.38744 0
680 2024-08-12 buy 90 490.070007 436.08681 90
Here I wanted to try reversing the strategy instead. What would happen if I bought on Fridays, hold for a week, and sell everything on Mondays instead? The results are honestly shocking. Like the previous scenarios, we start off with $10,000 and after 14 years we would end up with only $16980.92. A measly 69.81% gain.
>>> strategy(voo, buy=4, sell=0, hold=1).tail()
date trade qty price balance holdings
676 2024-07-19 buy 33 504.549988 452.377372 33
677 2024-07-29 sell 33 500.700012 16975.477768 0
678 2024-08-02 buy 34 489.910004 318.537632 34
679 2024-08-12 sell 34 490.070007 16980.917870 0
680 2024-08-16 buy 33 509.450012 169.067474 33
Baseline
From just looking at the strategy’s performance on it’s own it looks like it did exceedingly well. After all, 445.42% is a better return than anything I’ve ever managed myself. But what if I’d just bought and held the stock without any further trading?
# Starting with US$10,000, buy once and hold
def buy_n_hold(data:pd.DataFrame, date = None):
if date is None:
data_filtered = data.copy()
else:
data_filtered = data[data['Date'] >= date]
balance = 10000
buy_price = data_filtered.iloc[1].Close
qty = math.floor(balance/buy_price)
current_value = qty * data_filtered.iloc[-1]['Close']
return current_value
>>> print(buy_n_hold(voo))
49926.101176000004
Damn, if I’d bought $10,000 of VOO 14 years ago it would’ve ballooned in value by almost 500%.
HSI on the other hand…
>>> print(buy_n_hold(hsi))
4287.062
Visualization
Here I’ve plotted the strategy’s performance for different holding periods ranging from 0 to 2 weeks. I’ve also plotted the baseline and the reverse as a comparison.
VOO (US S&P500 ETF)
# Plot different holding periods function
def make_plot(ticker_dat:pd.DataFrame, holds:float, date:str=None): # date string format example '2024-07-01'
balance_baseline = buy_n_hold_tradebook(ticker_dat, date=date)[['date', 'balance']]
fig, ax = plt.subplots()
labels = []
for i in range(holds+1):
result_df = strategy(ticker_dat, hold=i, date=date)
balance_df = result_df[result_df['trade'] == 'sell'][['date', 'balance']]
labels.append(f'{i} week(s)')
# final_date = balance_df.iloc[-1].date
# final_balance = balance_df.iloc[-1].balance
ax.plot(balance_df['date'], balance_df['balance'])
# ax.annotate(text = round(final_balance, 2),
# xy = [final_date, final_balance],
# xytext = [(final_date+pd.Timedelta(20, unit='D')) , final_balance]
# )
# Add baseline
ax.plot(balance_baseline['date'], balance_baseline['balance'], linestyle='--')
# Add reverse
reverse_result = strategy(ticker_dat, buy=4, sell=0, hold=1, date=date)
reverse_balance_df = reverse_result[reverse_result['trade'] == 'sell'][['date', 'balance']]
ax.plot(reverse_balance_df['date'], reverse_balance_df['balance'], linestyle='-.')
# Set fig properties
ax.set(xlabel = 'Year\n\nData from Yahoo Finance | Visualized by Chi Kit Yeung',
ylabel = 'Balance ($)'
)
ax.set_title('Day-of-Week Strategy Different Holding Period Performance\nOverall balance over time', loc='left', fontsize=15)
ax.legend(title='Holding Period',
labels=labels + ['Buy and Hold', 'Reverse']
)
fig.set_figwidth(8)
fig.set_figheight(5)
make_plot(voo, 2, '2020-01-01')
7200.HK (Hang Seng Index ETF)
make_plot(hsi, 2, '2020-01-01')
FLIN (Franklin FTSE India ETF)
I’m including FLIN because it has come to my attention that the Indian market has been performing quite well. It could be a potential addition to my current DCA stocks.
make_plot(flin, 2, '2020-01-01')
Conclusion
This shows that while the Day of Week does have an effect on long term returns which is clearly evident in the case of VOO where the reverse of it performed significantly poorer compared to following the strategy (69.81% vs. 445.42%), the performance still follows closely with the stock’s overall trajectory. If the stock is a poor performer in general, in this example looking at HSI, the strategy will not magically give a good return. On the other hand, this also shows that while the strategy does not significantly help, it also doesn’t hurt either. Moving forward, I would try to time my monthly DCA strategy to purchase on Mondays and I would also look for a good time to exit from HSI as it is not a good long-term investment option. I’m also definitely going to look into adding FLIN into my portfolio.
Turn-of-the-Month Strategy
To be continued…
- Posted on:
- August 18, 2024
- Length:
- 9 minute read, 1709 words
- Tags:
- Personal Finance Investing
- See Also: