Blogs

Backtesting Optimized Technical Analysis Strategy

First, import the neccesary libraries:

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

Then, get historical prices using pdr from Yahoo Finance:

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

Trading strategy is simply buying when moving average is above the price and closing position when moving average is below the price. There is no short selling. For optimization 20 different moving averages will be used. 

num_sma = 20
window_range = np.linspace(2, num_sma*2, num_sma, dtype=int)

for window in window_range:
    
    sma = aapl.Close.rolling(window).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)

    positions = pd.Series(signals).shift(1)
    
    aapl[str(window) + '_sma'] = positions.values

aapl.dropna(inplace=True) #dropna before calculating detrended returns

Calculate historical detrended returns to calculate strategy returns:

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)

Create 500 simulated historical returns for the stock:

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)

Calculate all moving average strategy's returns and find the highest return. Then for each simulation of historical returns do the same. 

sma_columns = ['sma' in col for col in aapl.columns]
all_positions = aapl.loc[:, columns]
cumret = aapl.detrended_logret @ all_positions
yrl_rets = cumret / len(all_positions) * 252
test_return = max(yrl_rets)

sims_rets = ((sims @ all_positions.values) / len(all_positions) * 252)
sims_best_rets = sims_rets.max(axis=1)

Compare the original strategy return with simulations in graph. 

plt.hist(simulated_best_returns, 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()

Even though the optimum strategy (moving average that gave the highest return) gave approximatly 12% annual average return, compare to simulations it isn't a superior result. The test result says that the optimum strategy has no predictive power. 

Note on Optimization Bias:

When testing optimized strategies it is important to eliminate optimization bias. After selecting the optimized moving average (in this case it is 18-days moving average) if we test this strategy against its simulated returns we ignore the optimizition bias. The result looks more significant due to the bias:

sma18_sims_rets = ((sims @ aapl['18_sma'].values) / len(all_positions)) * 252

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