Blogs

Backtesting Simple Technical Analysis Strategy

First import usual suspects. Also, pdr is used to get financial data from yahoo. 

import pandas_datareader.data as pdr
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

Get historical data. Apple's last 4 years of prices in this case.

aapl = pdr.DataReader('AAPL', 'yahoo', start='2018-07-12', end='2022-06-30')

Trading strategy uses simple moving average. If price is above moving average buy the stock. When it goes below the moving average, close the position. There is no short selling involved. 

sma = aapl.Close.rolling(14).mean()
signals = []

for day_price, day_sma in zip(aapl.Close, sma):
    if day_price < day_sma:
        signals.append(0)
    elif day_price >= day_sma:
        signals.append(1)
    else:
        signals.append(np.nan)

aapl['sma_positions'] = pd.Series(signals).shift(1).values
aapl.dropna(inplace=True) #dropna before calculating detrended returns

To calculate strategy return, use detrended returns. Detrended returns eliminate the position bias. 

logret = np.log(aapl.Close / aapl.Close.shift(1)).rename('return')
avg_return = logret.mean()
aapl['detrended_logret'] = (logret - avg_return).values
aapl.dropna(inplace=True)

test_return = (aapl['sma_positions'] * aapl['detrended_logret']).mean()*252
print(test_return)

The result is approximately 10% annual average return. Does it mean this is a good strategy? To answer this a question significance test is required. Using monte carlo permutations we will create simulated samples of historical stock prices. 

num_days = len(aapl.detrended_logret)
mu = aapl.detrended_logret.mean()
sigma = aapl.detrended_logret.std()
np.random.seed(101)
all_sims = []
run = 500

for _ in range(run):
    sim_rets = np.random.normal(loc=mu, scale=sigma, size=num_days)
    all_sims.append(sim_rets)

sims = pd.DataFrame(all_sims)
sims_cumret = sims @ aapl['sma_positions'].values
sims_rets = sims_cumret / len(aapl['sma_positions']) * 252

As a result we get 500 simulation of historical aapl prices. Using each simulation we calculate our strategy's return. Ploting simulated returns with the original return will show the significance of result. 

plt.hist(sims_rets, bins=50)
plt.axvline(x=test_return, color='r', linestyle='--')
plt.title('Simulated Returns vs. Strategy Return')
plt.xlabel('Yearly Avg Returns')
plt.ylabel('Frequency')
plt.show()

Based on the comparison with simulations, 10% return can be attributed to chance. Thus, the strategy shows no predictive power.