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)

πŸ“Š Viz

Strategy Performance on VOO

<> Code

# 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)

πŸ“Š Viz

Strategy Performance on HSI

<> Code

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.

πŸ“Š Viz

Strategy Performance on FLIN

<> Code

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
Categories:
Python Investing
Tags:
Personal Finance Investing
See Also: