You are on page 1of 24

October Edition

Equity Quant Report

The MACD: The research on its validity and optimal


holding period using historical S&P500 stock data
HKU Trading Group Quant Team
Jinho Kwak

The MACD: the research on its validity and optimal holding period
using historical SP500 stock data

About
For almost every signal-producing technical indicator for stocks and under a seemingly
“efficient” market, people question the technical indicator’s validity and optimal holding period
when some signal is produced from it. In this research, the famous MACD momentum indicator
is examined using SP500 stock performance history from the years 2005.1~2021.8 with 2
central propositions:

- Is holding SP500 stock with “Buy” MACD signal better performing than SP500 index?
- If holding SP500 stock with “Buy” MACD signal is better than other groups, what is the
optimal holding period after getting “Buy” MACD signal in terms of risk-return
characteristics?

For reading the results only, please refer to the “results” and “annex” sections.

Theoretical Background
Moving Average Convergence Divergence (MACD) is a well-known trend-following
momentum indicator that shows the relationship between two moving averages of a security’s
price (Investopedia, 2021). Before calculating the aforementioned 2 statistics, it is crucial to
understand the concept of Exponential Moving Average (EMA).

An Exponential Moving Average (EMA) is a type of moving average that places a greater
weight and significance on recent data points. The formula for EMA is as follows:
The value input is usually the stock price. The most widely used smoothing factor - which is
also used for the calculation of MACD of this research - is 2. When the smoothing factor is
increased, recent stock price data fluctuation gives more influence to the EMA.

It is true that the calculation of EMA is viable for almost every time-series datasets. The value
input is not limited to securities price, but also secondary statistics such as MACD.

With this concept of EMA introduced, we can introduce the widely used formula for three
statistics critical to the MACD analysis: the MACD line, signal line, and MACD Histogram Line.
- MACD Line = EMA(12) - EMA(26), where value input for the EMA here is stock price
and numbers in the parenthesis signify the input for “Days”
- Signal Line = EMA(9), where value input for the EMA here is “MACD line”. It is the EMA
of MACD statistics.
- MACD Histogram Line = MACD Line - Signal Line

MACD yields buy/sell signals when:


MACD Histogram value changes from negative to positive → Buy Signal
MACD Histogram value changes from positive to negative → Sell Signal

Methodology
Overview
We compare the equal-weighted return of portfolios of SP500 stocks with MACD “buy”
signals and RSP (Invesco S&P 500® Equal Weight ETF). If the portfolio with any holding
period generates greater return than RSP, it would show the validity of MACD signal. The
followings are also considered in the analysis:
- Transaction & Slippage Cost
- Penny Stocks excluded (stocks that were priced not less than a dollar at the start of the
holding period)
- The seed money (starting capital) is set to be 100,000$ for both MACD strategy and
the investment in RSP.

At the same time, we try to regularly re-construct and hold the portfolio of stocks with MACD
“buy” signals weekly, bi-weekly, tri-weekly, monthly, and bi-monthly, and try to hold it starting
from different days (first 5 days). (see explanation below)
Weekly means that we try to look at MACD values weekly for each stock in S&P 500, select
the ones that had “buy” signs to construct a portfolio, and hold it from the next trading day for
a trading week.

Example for first~fifth day for weekly analysis:


- First Day
- First Portfolio: check MACD sign for January 15, hold for a trading week starting
from a trading day after January 15.
- Second Portfolio: check MACD sign for January 22. Hold for a trading week
starting from a trading day after January 22, and so on.
- Second Day
- First Portfolio: check MACD sign for January 16, hold for a trading week starting
from a trading day after January 16.
- Second Portfolio: check MACD sign for January 23. Hold for a trading week
starting from a trading day after January 23, and so on.

The main reason for this is to cover more dates and MACD signals as a single analysis period
based on MACD signal at one date may create biased data.

For return analysis, we calculated the median annualized expected return to exclude the effect
of outliers.

Calculation Method for median annualized expected return:


- Median annualized expected return =
(% success rate) * (median annualized return for successful trades) + (1 - % success
rate) * (median annualized return for unsuccessful trades)
- % success rate: % of portfolio returns that exceeded RSP return during the same
holding period
- Annualized return: portfolio return ^ compounding period per year
- For example, when we analyze weekly portfolio holding strategy,
portfolio return will be for 1 week and compounding period will be 52.

Also, we calculated the median sharpe ratio for each portfolio holding strategy based on cost
adjusted returns to see the risk-return characteristics.
Step 1: Installation of libraries & installation of alpha vantage(data source)
We import the necessary libraries to accommodate the data acquisition:
- Numpy: a tool to modify array-based numpy data structure for data analysis
- Google.colab.drive: to access personal google drive to save source data for MACD and
others for faster access
- Pandas: a tool to modify the DataFrame, a Numpy based data structure
- Os: to create complete file path when accessing csv data
- Requests: to request for html response to the internet with the given URL (you can
think this as a tool to access the web pages to scrap some/all of its contents)
- BeautifulSoup: a tool to modify & select among html web response
- Datetime: for modifying pandas & numpy data structures using time-series based
indices
- Progressbar: an interface to show progress of data acquisition / analysis

import numpy as np
from google.colab import drive
import pandas as pd
import os
import requests
from bs4 import BeautifulSoup
import datetime
import progressbar

Then, we specify path where csv files be saved / re-accessed:


path = '/content/gdrive/MyDrive'

To successfully save csv data files in the personal google drive, we need to mount the drive:
drive.mount('/content/gdrive')

Lastly, we install alpha vantage, a helpful resource for us to retrieve technical and
fundamental stock information.
pip install alpha_vantage

Step 2: Acquisition of historical constituents for S&P 500 stocks


To perform backtesting on S&P 500 stocks, the list of historical constituents is needed to
remove survivorship bias. Survivorship bias occurs when an investigator tries to analyze and
hold only current SP500 constituents only. Such action disregards the stocks that had gone
bust in the past, possibly overstating the overall return.
For the use of the public, the acquisition of historical SP500 constituents is done by extracting
data from Wikipedia page named <List of S&P 500 companies> With the historical constituent
addition/ deletion history and current SP500 constituent list, we can acquire the proxy for the
desired historical constituent list.

First, we proceed to request data using the URL of Wikipedia <List of S&P 500 companies>,
and source the table where the addition/ deletion history exists using BeautifulSoup library:
print("Sourcing historical stock list for SP500...")
htmlobj = requests.get('http://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
soup = BeautifulSoup(htmlobj.text,'html.parser')
table = soup.find('table',{'class':'wikitable sortable'})

We use simpler method from pandas library to source the current constituent list for S&P 500
companies:
currentlist =pd.read_html(
'http://en.wikipedia.org/wiki/List_of_S%26P_500_companies')[0]['Symbol']
currentlist = pd.DataFrame(currentlist).T

After sourcing two tables, we did the following for easier analysis of time-series data:

Table for addition/ deletion history: The index column of addition/ deletion history contains
string format of the dates i.e. May 14, 2021. We need to convert them into datetime objects for
easier indexing & analysis.
date_list = {'January':1,'February':2, 'March':3, 'April':4, 'May':5, 'June':6, 'July':7, 'August':8,
'September':9,
'October':10, 'November':11, 'December':12}

for i in range(0,table.shape[0]):
tempdate = table.iloc[i,0].split()
table.iloc[i,0] = datetime.datetime(year=int(tempdate[2]), month=date_list[tempdate[0]],
day=int(tempdate[1].replace(',','')))

Table for current S&P 500 constituents: We have left the table leaving only the necessary
information (deleted ticker, added ticker and Date). We also set the date as an index of the
resulting DataFrame.
table.columns = ['Date', 'Added','Security','Deleted','Security','Reason']
table = table[['Date','Added','Deleted']]
table.set_index('Date')

After formatting the tables, we proceeded to make a list of historical SP500 constituents by
looping through the addition & deletion history, starting from current SP500 constituents.
tickers = []
[tickers] = currentlist.values.tolist()
historicaltick = pd.DataFrame([tickers],index=[datetime.datetime.now()])

for index, row in table.iterrows():


if row['Added'] in set(tickers):
tickers.remove(row['Added'])
if row['Deleted'] not in set(tickers):
tickers.append(row['Deleted'])
temp = pd.DataFrame([tickers],index=[row['Date']-datetime.timedelta(1)])
historicaltick = pd.concat([historicaltick,temp],axis=0)

historicaltick = historicaltick[~historicaltick.index.duplicated(keep='first')]
historicaltick['Date'] = historicaltick.index.values
historicaltick = historicaltick.resample('1D').first().fillna(method='bfill')
historicaltick.fillna(0)

print("Retrieved the historical stock list for SP500")


historicaltick.to_csv(os.path.join(path,"historicalSP500.csv"))

For sourcing the source data for time-series MACD signal datasets, we need to acquire all
stock tickers that have been listed at least once in S&P 500 during the period of analysis. To do
this, just combine the row information that contains the information for S&P 500 constituents
and delete the redundant tickers:
df = pd.read_csv(os.path.join(path, 'historicalSP500.csv'), index_col='Unnamed: 0')
df.index = df['Date']
tickerlist = []
for col in df.loc[:,df.columns != 'Date'].columns:
tickerlist = tickerlist + df[col].unique().tolist()
tickerlist = [x for x in list(set(tickerlist)) if str(x) != 'nan']

Step 3: Acquisition of historical MACD signal information for S&P 500 stocks
First, we created the empty dataframe that will contain the MACD information we need. As
this process takes around 30 minutes, we also added a progress bar to check the status of this
data acquisition. We also declared excl_stock list to store tickers of stocks that had no
information or generated errors during the process.
MACDdata = pd.DataFrame()

bar = progressbar.ProgressBar(maxval=len(tickerlist), widgets=[progressbar.Bar('=', '[', ']'), ' ',


progressbar.Percentage()])

bar.start()

excl_stock = []

Then, we proceeded to source MACD histogram data from alpha vantage, compare that with
MACD histogram data from previous day, and return the trading decision (Buy/Sell/Neutral) for
each ticker:
for ticker, i in zip(tickerlist,range(len(tickerlist))):
try:
##source MACD data from Alpha Vantage
url = 'https://www.alphavantage.co/query?function=MACD&symbol='
+ticker+'&interval=daily&series_type=close&apikey='+key

urlData = requests.get(url).json()["Technical Analysis: MACD"] ##convert to dict type python object


for easier manipulation

##Cleaning data
df = pd.DataFrame(urlData).T
df = df["MACD_Hist"]
df = df.rename(ticker)

##create separate column for previous day's MACD Historam Data for comparison
df = pd.DataFrame({ticker:df.astype(float), 'prev':df.shift(periods=-1).astype(float)})

##get the product of previous day's MACD Historgam data with that of current day
df['cur*prev'] = df[ticker]*df['prev']

##if the product acquired above is greater than 0, neutral signal is produced.
##otherwise, it indicates that the sign has changed and we have to additionally check whether it is
a buy / sell signal
df['Result'] = np.where((df['cur*prev'] < 0) & (df[ticker] < 0), 'Buy' , np.where((df['cur*prev'] <
0) & (df[ticker] > 0), 'Sell', 'Neutral'))
df[ticker] = df['Result']
df = df[ticker]

##add completed data for a ticker


MACDdata = MACDdata.merge(df, how='outer', left_index=True, right_index=True)

##update progressbar
bar.update(i+1)

##if there's any error, put the erroneous stock ticker in excl_stock list
except:
excl_stock.append(ticker)
bar.update(i+1)

Lastly, we have saved the MACD information data to the personal google drive for further use
and printed excluded stocks:
print('exception: ')
print(excl_stock)
MACDdata.to_csv(os.path.join(path,"MACDdata.csv"))

Step 4: Periodical MACD buy signal portfolio construction & return analysis
Before proceeding to the analysis, we specify the parameters for the return analysis:
- Startdate: to specify the starting date of analysis
- Enddate: to specify the end date of analysis
- Frequency: frequency for constructing & holding the portfolio. Please refer to the
overview part of this report for details. The permissible periods are:W for week, M for
month. Scalar multiples are allowed.
- Comp_period: compounding period per year for return analysis. For example, when
doing weekly analysis, the comp_period should be “52” since there are 52 trading
weeks per year.
startdate = '2005-01-01'
enddate = '2021-08-31'

frequency = 'W'
comp_period = 52

Then, we create empty dataframe with specified date frequency to later store the portfolio
return and sharpe ratio data:
result = pd.DataFrame(index=pd.date_range(startdate, enddate, freq=frequency))
result.index.name = 'Period Starting'
result['Return'] = np.nan
result['Sharpe Ratio'] = np.nan

We also retrieve the MACD signal data that we acquired from step 4. With this code, it is
recommended that the researcher skip step 3 once done for faster analysis.
MACDdata = pd.read_csv(os.path.join(path, "MACDdata.csv"), index_col='Unnamed: 0')
MACDdata.index = pd.to_datetime(MACDdata.index)

We also retrieve the historical stock data for S&P 500 constituents, and re-index as datetime
format:
df = pd.read_csv(os.path.join(path, "sp500stockpricehist.csv"), index_col='Unnamed: 0')
df.index = pd.to_datetime(df.index)

For calculating sharpe ratio, we need time-series data for risk-free rate. To do this, we have
imported historical US 10-year treasury yield data for use in the later part of the analysis.
url = 'https://www.alphavantage.co/query?function=TREASURY_YIELD&interval=daily&maturity=10year&apikey='
+ key
urlData = requests.get(url).json()['data']
treasury_df = pd.DataFrame(urlData)
treasury_df.index = treasury_df['date']
del treasury_df['date']
treasury_df.index = [datetime.datetime.strptime(idx,'%Y-%m-%d') for idx in treasury_df.index]
treasury_df[treasury_df['value']=='.'] = np.nan
treasury_df[treasury_df['value']=='nan'] = np.nan
treasury_df.fillna(method='ffill')
We also declare excl_list, for similar use we have outlined in the earlier part of the research
paper:
excl_list = []

We have included a time shift parameter to accommodate periodical analysis starting from
different dates. For example, the value 1 makes it start from the first day, 2 for second day, and
so on.
- Example: if 1:
- 2005.01.01 ~ 2005.01.07
- 2005.01.08 ~ 2005.01.14
- …
- If 2:
- 2005.01.02 ~ 2005.01.08
- 2005.01.09 ~ 2005.01.15
- …

Then, we selected stocks that had “buy” MACD signal 1 trading day before the start of
portfolio holding period, held the stocks for the specified period, and calculated equal
weighted return and sharpe ratio per each period of strategy.

We also included slippage cost of 0.02 dollars per transaction, and applied transaction cost of
Interactive Brokers with charges a maximum between (size of stock order * 0.005) and 1 for
each particular stock:
time_shift = 5
for endperiod, startperiod in zip(result.index.tolist()[:-1],result.index.tolist()[1:]):
temp = MACDdata.loc[:endperiod+datetime.timedelta(time_shift-1)].tail(1).transpose().copy()
MACDselect = temp[temp[temp.columns] == 'Buy'].dropna().index.tolist()
MACDanalysisdate = temp[temp[temp.columns] == 'Buy'].columns[0].strftime("%Y/%m/%d")

if len(MACDselect) == 0: ##if no stock had buy MACD signal


result.loc[endperiod] = 0.0

else:
stock_price_list = pd.DataFrame()
for ticker in MACDselect:
try:
perioddata =
df[ticker].loc[endperiod+datetime.timedelta(time_shift):startperiod+datetime.timedelta(time_shift)]
if perioddata[0] > 1.0: ##exclude penny stocks
stock_price_list = pd.concat([stock_price_list, pd.DataFrame({ticker:perioddata},
index=perioddata.index)], axis=1)
except: ##error handling
excl_list.append(ticker)
if len(stock_price_list.columns.tolist()) == 0:
result.loc[endperiod, "Non-cost Return"] = 0.0 ##if there were no stocks that had buy signals
result.loc[endperiod, 'Number of Stocks'] = 0.0
result.loc[endperiod, 'Position Size'] = 0.0
continue

portfolio_return = stock_price_list.apply(lambda x: x[-1]/x[0]-1).mean() ##mean return


result.loc[endperiod, 'Number of Stocks'] = len(stock_price_list.columns.tolist())
result.loc[endperiod, 'Position Size'] = stock_price_list.iloc[0].sum()

length = len(stock_price_list.columns.tolist())
weights = np.full(shape=(1, length), fill_value=1/length)

result.loc[endperiod, 'Treasury_yield'] =
(((float(treasury_df.loc[perioddata.index.tolist()[-1]][0])/100)+1)**(1/comp_period)-1)
result.loc[endperiod, 'portfolio_sd'] = np.dot(np.dot(weights,
stock_price_list.pct_change(periods=1).cov()),np.transpose(weights))**0.5 ##portfolio standard deviation

#result.loc[endperiod, "Sharpe Ratio"] = sharpe_ratio


result.loc[endperiod, "Non-cost Return"] = portfolio_return

##Apply Transaction Costs, assume that we start at 100,000 dollars


start = 100000.0

initialdf = pd.DataFrame(np.full(shape=(1,len(result.columns.tolist())), fill_value=0),


columns=result.columns, index=['Initial Value'])
result = pd.concat([initialdf, result])
result.loc['Initial Value', 'Cost Adjusted Value'] = start
result[['Sharpe Ratio', 'Cost Adjusted Return', 'Size Per Position']] = np.nan

for row, nextrow in zip(result.iloc[:-1].iterrows(), result.iloc[1:].iterrows()):


if nextrow[1]['Number of Stocks'] == 0:
nextrow[1]['Cost Adjusted Value'] = row[1]['Cost Adjusted Value']
nextrow[1]['Cost Adjusted Return'] = 0.0
continue

nextrow[1]['Size Per Position'] = row[1]['Cost Adjusted Value']/nextrow[1]['Position Size']

transaction_cost = max(1*nextrow[1]['Number of Stocks'], nextrow[1]['Size Per


Position']*0.005*nextrow[1]['Number of Stocks']) ##transaction cost based on interactive brokers

slippage_cost = (row[1]['Cost Adjusted Value']/nextrow[1]['Position Size'])*nextrow[1]['Number of


Stocks'] * 0.04 ##Slippage Cost for buying and selling (0.02 dollars for each buying and selling)

nextrow[1]['Cost Adjusted Value'] = (row[1]['Cost Adjusted Value']*(nextrow[1]['Non-cost Return'] +


1)) - slippage_cost - transaction_cost

nextrow[1]['Cost Adjusted Return'] = (nextrow[1]['Cost Adjusted Value'] / row[1]['Cost Adjusted


Value']) - 1

treasury_yield = nextrow[1]['Treasury_yield']
sharpe_ratio = (nextrow[1]['Cost Adjusted Return'] - treasury_yield)/nextrow[1]['portfolio_sd']
nextrow[1]['Sharpe Ratio'] = sharpe_ratio

result.drop('Initial Value', inplace=True)


result.drop(['Treasury_yield', 'portfolio_sd', 'Number of Stocks', 'Non-cost Return'], axis=1,
inplace=True)

##print excluded stocks


print(list(set(excl_list)))
We have done the similar analysis using frequency input for RSP, a equal weighted S&P 500
portfolio:
result_RSP = pd.DataFrame(index=pd.date_range(startdate, enddate, freq=frequency))
result_RSP.index.name = 'Period Starting'
result_RSP['Return'] = np.nan

url =
'https://www.alphavantage.co/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol=RSP&outputsize=full&interv
al=daily&series_type=close&apikey=0D6HAHFDZPJLAL0E'
urlData = requests.get(url).json()['Time Series (Daily)']
df = pd.DataFrame(urlData).T
df.index = [datetime.datetime.strptime(idx,'%Y-%m-%d') for idx in df.index]
df = df['5. adjusted close']

for endperiod, startperiod in zip(result.index.tolist()[:-1],result.index.tolist()[1:]):


stock_return_list = pd.DataFrame()

ticker = 'RSP'
perioddata =
df.loc[startperiod+datetime.timedelta(time_shift):endperiod+datetime.timedelta(time_shift)]
stock_return = pd.DataFrame({ticker:[(float(perioddata[0]) / float(perioddata[-1])) - 1]},
index=[endperiod])
stock_return_list = pd.concat([stock_return_list, stock_return], axis=1)

result_RSP.loc[endperiod] = stock_return_list.mean(axis=1)[0]

After getting portfolio return & RSP return information, we form a final result table to make as
a reference to the return comparison graph we will create later. For easier comparison, we
made RSP starting from 1 on January 1, 2005 and continuously multiplied by the returns to
generate the return graph:
final_result = pd.DataFrame({'RSP':((result_RSP + 1).cumprod())['Return']*100000.0,
'MACD_Portfolio':result['Cost Adjusted Value']}, index = result.index)

Step 5: Visualization of results


First, we visualize the distribution of portfolio annualized returns using matplotlib:
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()

fig.set_figheight(10)
fig.set_figwidth(20)

plt.xlim((-1,2))
plt.ylim((0,0.13))
result['Annualized_Return'] = (result['Cost Adjusted Return'] + 1)**(comp_period)
plt.hist(result['Annualized_Return'].sort_values()-1.0, bins=50, range=(-1,2),
weights=np.zeros_like(result['Annualized_Return'].sort_values()) + 1. /
result['Annualized_Return'].sort_values().size)
plt.show()

Using the final_result dataframe, return, and Sharpe ratio information, we create a graph to
compare returns for RSP and specified portfolio strategy, and create % of successful trades.
fig, ax1 = plt.subplots()
fig.set_figwidth(20)
fig.set_figheight(10)

plt.plot(final_result.index, final_result['MACD_Portfolio'].astype(float), label='MACD Portfolio',


color='blue')
plt.plot(final_result.index, final_result['RSP'].astype(float), label='RSP', color='red')

plt.xlabel('Date')
plt.ylabel('Price (Starting Value = 100,000 dollars)', color = 'red')
plt.legend()
plt.show()

print("Strategy Outperformed RSP by: " + str(round((final_result['MACD_Portfolio'][-2] -


final_result['RSP'][-2]), 2)) + " $")

print("% of successful trades: " + str(round((len(result.loc[result_RSP['Return'] < result['Cost


Adjusted Return']].index.tolist())/len(result.index.tolist()))*100,2))+ "%")

print("Annualized Median Return for successful trades: " + str(round(((result.loc[result_RSP['Return'] <


result['Cost Adjusted Return']]['Cost Adjusted Return'].median()+1)**comp_period-1)*100,2)) + "%")

print("Annualized Median Return for unsuccessful trades: " + str(round(((result.loc[result_RSP['Return']


> result['Cost Adjusted Return']]['Cost Adjusted Return'].median()+1)**comp_period-1)*100,2)) + "%")

print("Median Annualized return: " + str(round((result['Annualized_Return'].median(axis=0)-1)*100,2)) +


"%")

print("Annualized Median Sharpe ratio: " + str(round(result['Sharpe


Ratio'].median()*(comp_period**0.5),2)))
Results
Table Summary of Results
Weekly
Holding Period starting on: First day Second day Third day Fourth day Fifth day
Strategy Outperformed RSP by 632262.84 $ -49885.56 $ 117332.96 $ 679542.83 $ 47160.76 $
% of successful trades 49.89% 49.08% 47.82% 50.69% 48.28%
Annualized Median Return for successful trades 84.02% 75.51% 92.16% 78.91% 58.10%
Annualized Median Return for unsuccessful trades -18.68% -15.54% -19.50% -18.24% -10.49%
Median Annualized Return 9.73% 15.93% 20.99% 21.13% 19.70%
Annualized Median Sharpe Ratio 1.36 2.65 3.28 2.76 2.71
Expected Annualized Median Return per trial 32.56% 29.15% 33.90% 31.01% 22.63%
*using % of successful trades as probability Average 29.85%

Bi-Weekly
Holding Period starting on: First day Second day Third day Fourth day Fifth day
Strategy Outperformed RSP by 340837.55 $ 253428.37 $ 109061.62 $ 34906.85 $ -265283.81 $
% of successful trades 52.41% 49.66% 48.97% 52.41% 47.82%
Annualized Median Return for successful trades 60.48% 66.57% 62.48% 54.89% 48.60%
Annualized Median Return for unsuccessful trades -5.29% -6.38% -9.67% -2.01% -5.95%
Median Annualized Return 21.83% 21.07% 17.76% 18.44% 15.93%
Annualized Median Sharpe Ratio 4.66 3.99 4.36 3.18 3.91
Expected Annualized Median Return per trial 29.18% 29.85% 25.66% 27.81% 20.14%
*using % of successful trades as probability Average 26.53%

Tri-Weekly
Holding Period starting on: First day Second day Third day Fourth day Fifth day
Strategy Outperformed RSP by 241658.32 $ 289693.11 $ 277006.33 $ 45644.4 $ -56663.26 $
% of successful trades 52.41% 50.00% 51.72% 54.14% 56.21%
Annualized Median Return for successful trades 58.62% 54.60% 53.54% 37.87% 52.44%
Annualized Median Return for unsuccessful trades -9.86% -2.18% -0.52% -1.84% -6.51%
Median Annualized Return 22.18% 14.86% 16.78% 19.54% 22.72%
Annualized Median Sharpe Ratio 4.77 3.58 3.59 3.82 5.22
Expected Annualized Median Return per trial 26.03% 26.21% 27.44% 19.66% 26.63%
*using % of successful trades as probability Average 25.19%
Monthly
Holding Period starting on: First day Second day Third day Fourth day Fifth day
Strategy Outperformed RSP by 322051.11 $ 657023.53 $ 273406.19 $ 83823.7 $ 218396.1 $
% of successful trades 48.00% 54.00% 53.50% 61.00% 56.50%
Annualized Median Return for successful trades 43.86% 41.85% 38.10% 46.77% 41.52%
Annualized Median Return for unsuccessful trades 0.00% -3.37% -2.15% -3.98% -0.65%
Median Annualized Return 18.58% 23.35% 18.12% 18.41% 17.27%
Annualized Median Sharpe Ratio 5.01 7.11 3.55 4.41 5.41
Expected Annualized Median Return per trial 21.05% 21.05% 19.38% 26.98% 23.18%
*using % of successful trades as probability Average 22.33%

Bi-Monthly
Holding Period starting on: First day Second day Third day Fourth day Fifth day
Strategy Outperformed RSP by 136437.98 $ 457406.82 $ 1757043.2 $ 592556.15 $ 1008127.94 $
% of successful trades 53.00% 55.00% 61.00% 58.00% 55.00%
Annualized Median Return for successful trades 38.73% 34.36% 43.94% 40.33% 43.78%
Annualized Median Return for unsuccessful trades 1.81% 3.32% 3.60% 7.55% 7.52%
Median Annualized Return 14.75% 20.56% 32.31% 22.91% 21.76%
Annualized Median Sharpe Ratio 6.89 6.9 9.56 8.46 7.68
Expected Annualized Median Return per trial 21.38% 20.39% 28.21% 26.56% 27.46%
*using % of successful trades as probability Average 24.80%
Comments
1. The portfolio strategy is generally profitable by outperforming the market (RSP) in
the long run. Out of 25 trials with different holding periods, 22 trials were able to beat
the market. The main reason is that the strategy secured a success probability of
around 50%, with greater weight on gain when it was successful than the amount lost
when failed. Although the strategy does not guarantee earnings in the short period of
time, If a trader continues investing on the “portfolio of stocks” with MACD buy signals,
he/she will have a decent chance of winning the market.

This is the example of portfolio strategy beating the market (Weekly Holding Period). Graphs
of all trials will be displayed after the results section as well.

2. Generally there were sharp spikes in portfolio return during early-mid 2020 even
greater than market rebound. Although further analysis is needed for the most
correlated reason, we suspect that the phenomenon is due to the entrance of more
retail momentum buyers in the market.

3. By referring to the expected median annualized returns, the weekly holding period
shows to be the best holding period when doing the MACD portfolio strategy.
Although the sharpe ratio may mislead the best portfolio strategy to be Bi-monthly, we
strongly suspect that the sharpe ratio difference is mainly due to the distortion by high
portfolio volatility caused by momentum buyers in the first few days after the MACD
buy signal is produced. The fact that the effect of Sharpe ratio for evaluation is limited
due to the non-normality of portfolio return distribution also adds to this argument.
Annex
Graphs of portfolio strategy
Weekly Portfolio Holding Period - First day

Annualized Return Distribution Portfolio return vs RSP

Weekly Portfolio Holding Period - Second day

Annualized Return Distribution Portfolio return vs RSP

Weekly Portfolio Holding Period - Third day

Annualized Return Distribution Portfolio return vs RSP


Weekly Portfolio Holding Period - Fourth day

Annualized Return Distribution Portfolio return vs RSP

Weekly Portfolio Holding Period - Fifth day

Annualized Return Distribution Portfolio return vs RSP

Bi-Weekly Portfolio Holding Period - First day

Annualized Return Distribution Portfolio return vs RSP

Bi-Weekly Portfolio Holding Period - Second day

Annualized Return Distribution Portfolio return vs RSP


Bi-Weekly Portfolio Holding Period - Third day

Annualized Return Distribution Portfolio return vs RSP

Bi-Weekly Portfolio Holding Period - Fourth day

Annualized Return Distribution Portfolio return vs RSP

Bi-Weekly Portfolio Holding Period - Fifth day

Annualized Return Distribution Portfolio return vs RSP

Tri-Weekly Portfolio Holding Period - First day

Annualized Return Distribution Portfolio return vs RSP


Tri-Weekly Portfolio Holding Period - Second day

Annualized Return Distribution Portfolio return vs RSP

Tri-Weekly Portfolio Holding Period - Third day

Annualized Return Distribution Portfolio return vs RSP

Tri-Weekly Portfolio Holding Period - Fourth day

Annualized Return Distribution Portfolio return vs RSP

Tri-Weekly Portfolio Holding Period - Fifth day

Annualized Return Distribution Portfolio return vs RSP


Monthly Portfolio Holding Period - First day

Annualized Return Distribution Portfolio return vs RSP

Monthly Portfolio Holding Period - Second day

Annualized Return Distribution Portfolio return vs RSP

Monthly Portfolio Holding Period - Third day

Annualized Return Distribution Portfolio return vs RSP

Monthly Portfolio Holding Period - Fourth day

Annualized Return Distribution Portfolio return vs RSP


Monthly Portfolio Holding Period - Fifth day

Annualized Return Distribution Portfolio return vs RSP

Bi-Monthly Portfolio Holding Period - First day

Annualized Return Distribution Portfolio return vs RSP

Bi-Monthly Portfolio Holding Period - Second day

Annualized Return Distribution Portfolio return vs RSP

Bi-Monthly Portfolio Holding Period - Third day

Annualized Return Distribution Portfolio return vs RSP


Bi-Monthly Portfolio Holding Period - Fourth day

Annualized Return Distribution Portfolio return vs RSP

Bi-Monthly Portfolio Holding Period - Fifth day

Annualized Return Distribution Portfolio return vs RSP


IMPORTANT
Disclaimer

NO INVESTMENT ADVICE
This report is for informational and academic purposes only, you should not construe any such
information or other material as legal, tax, investment, financial, or other advice. Nothing contained
in this report constitutes a solicitation, recommendation, endorsement, or offer by the author or any
third party service provider to buy or sell any securities or other financial instruments in this or in
any other jurisdiction in which such solicitation or offer would be unlawful under the securities laws
of such jurisdiction. All content in this report is information of a general nature and does not address
the circumstances of any particular individual or entity. Nothing in the report constitutes
professional and/or financial advice, nor does any information in this report constitutes a
comprehensive or complete statement of the matters discussed or the law relating thereto. The
author is not a fiduciary by virtue of any person’s use of or access to this report. You alone assume
the sole responsibility of evaluating the merits and risks associated with the use of any information
in this report before making any decisions based on this report. In exchange for accessing this
report, you agree not to hold the author, its affiliates or any third party service provider liable for
any possible claim for damages arising from any decision you make based on information made
available to you through this report.

INVESTMENT RISKS
There are risks associated with investing in securities. Investing in stocks, bonds, exchange traded
funds, mutual funds, and money market funds involve risk of loss. Loss of principal is possible. Some
high risk investments may use leverage, which will accentuate gains & losses. Foreign investing
involves special risks, including a greater volatility and political, economic and currency risks and
differences in accounting methods. A security’s or a firm’s past investment performance is not a
guarantee or predictor of future investment performance.

You might also like