Backtesting is used to evaluate the historical performance and risk of investment strategies. Backtesting shows us how an investment strategy would have performed if implemented in the past. After backtesting several strategies, an investor may choose their preferred strategy based on their own investment goals and risk tolerance.
To conduct a backtest, you need a dataset of historical adjusted prices for assets you are considering to add to your portfolio. The data set needs to span the time range and include the assets that you are interested in.
Let's use polygon.io for the financial data used in this demo. Their API is free to use if you have a brokerage account with Alpaca. The assets we will consider investing in are ETFs from Vanguard that cover equities within different industries in the United States, including technology, healthcare, and financial services. The ETFs I consider for my portfolio are:
import requests
import json
import time
import pandas as pd
import numpy as np
import pytz
import bt
from datetime import datetime
%matplotlib inline
# retrieve alpaca keys
# this defines a variable named KEY_ID, which contains my Alpaca Key ID
%run alpaca_keys.py
multiplier = '1'
date_start = '2006-01-01'
date_end = '2020-07-30'
timespan = 'day'
tickers = ['VGT', 'VNQ', 'VHT', 'VFH', 'VDC', 'VOX', 'VDE', 'VIS', 'VAW', 'VPU', 'VCR']
price_dict = {}
price_dict.update({'t': pd.DataFrame(requests.get(f'https://api.polygon.io/v2/aggs/ticker/VGT/range/{multiplier}/{timespan}/{date_start}/{date_end}?apiKey={KEY_ID}').json()['results'])['t']})
for ticker in tickers:
request_string = f'https://api.polygon.io/v2/aggs/ticker/{ticker}/range/{multiplier}/{timespan}/{date_start}/{date_end}?apiKey={KEY_ID}'
price_dict.update({ticker: pd.DataFrame(requests.get(request_string).json()['results'])['c']})
prices_df = pd.DataFrame(price_dict)
est = pytz.timezone('US/Eastern')
utc = pytz.utc
prices_df.index = [datetime.utcfromtimestamp(ts / 1000.).replace(tzinfo=utc).astimezone(est).date() for ts in prices_df['t']]
prices_df.index.name = 'Date'
prices_df.drop('t', axis=1, inplace=True)
prices_df.head()
# convert prices to daily returns in case I'd like to analyze this later
r_df = prices_df.pct_change().dropna()
# convert returns to hypothetical prices if each asset price started at $100
pdf = 100*np.cumprod(1+r_df)
# Change index to pandas Timestamps
prices_df.index = [pd.Timestamp(x) for x in prices_df.index]
pdf.index = [pd.Timestamp(x) for x in pdf.index]
pdf.plot(figsize=(20,6))
Strategy 1: Assign equal portfolio weights for each asset. Rebalance the portfolio every year.
s1 = bt.Strategy(
name='EW',
algos=[
bt.algos.RunYearly(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
bt.algos.Rebalance()]
)
test1 = bt.Backtest(s1, prices_df, integer_positions=False)
Strategy 2: Assign portfolio weights that are proportional to the inverse of the asset's variance over the past 90 days. Rebalance every quarter.
s2 = bt.Strategy(
name='InverseVol',
algos=[
bt.algos.RunQuarterly(),
bt.algos.SelectAll(),
bt.algos.WeighInvVol(lookback=pd.DateOffset(days=90)),
bt.algos.Rebalance()]
)
test2 = bt.Backtest(s2, prices_df, integer_positions=False)
Strategy 3: Assign portfolio weights to make each asset contribute an equal amount of risk to the portfolio. Rebalance every quarter.
s3 = bt.Strategy(
name='EqualRisk',
algos=[
bt.algos.RunQuarterly(),
bt.algos.SelectHasData(lookback=pd.DateOffset(days=90)),
bt.algos.WeighERC(lookback=pd.DateOffset(days=90)),
bt.algos.Rebalance()]
)
test3 = bt.Backtest(s3, prices_df, integer_positions=False)
Strategy 4: Assign portfolio weights to maximize expected returned normalized by variance. Rebalance every month.
s4 = bt.Strategy(
name='MeanVarOpt',
algos=[
bt.algos.RunMonthly(),
bt.algos.SelectHasData(lookback=pd.DateOffset(days=90)),
bt.algos.WeighMeanVar(lookback=pd.DateOffset(days=90), rf=0),
bt.algos.Rebalance()]
)
test4 = bt.Backtest(s4, prices_df, integer_positions=False)
Strategy 5: Assign constant weights for a subset of the assets. 60% to tech, 20% to healthcare, and 20% to industrials. Rebalance every month.
# tickers = ['VGT', 'VNQ', 'VHT', 'VFH', 'VDC', 'VOX', 'VDE', 'VIS', 'VAW', 'VPU', 'VCR']
# 60% to VGT, 20% to VHT, and 20% to VIS
weights_s5 = pd.Series([0.6, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0], index=tickers)
s5 = bt.Strategy(
name='TechHeavy',
algos=[
bt.algos.RunMonthly(),
bt.algos.SelectAll(),
bt.algos.WeighSpecified(**weights_s5),
bt.algos.Rebalance()]
)
test5 = bt.Backtest(s5, prices_df, integer_positions=False)
Strategy 6: Invest in assets above their 50-day moving average.
# Consider all assets where the price is above its 50-day moving average and weight them equally in your portfolio.
# Rebalance monthly
# Daily, find all assets that have prices above their 50-day moving average.
# Among this subset, assign weights equally.
ma50 = pdf.rolling(50).mean()
s6 = bt.Strategy(
name='Above50MA',
algos=[
bt.algos.RunDaily(run_on_first_date=False),
bt.algos.SelectWhere(pdf > ma50),
bt.algos.WeighEqually(),
bt.algos.Rebalance()]
)
test6 = bt.Backtest(s6, prices_df, integer_positions=False)
Strategy 7: Invest in assets when their 50-day moving average crosses above their 200-day moving average.
# Moving average cross over strategy.
# When the 50-day moving average crosses above the 200-day moving average,
# purchase that stock.
# When it cross back, sell.
ma50 = pdf.rolling(50).mean()
ma200 = pdf.rolling(200).mean()
tw = ma200.copy()
# hold a position in an asset when its 50-day moving average is above its 200-day moving average
tw[ma50 > ma200] = 1
tw[ma50 <= ma200] = 0.0
tw[ma200.isnull()] = 0.0
# make each row sum to 1 (or 0 if no assets meet criteria)
tw = tw.div(tw.sum(axis=1), axis=0, )
ma_cross = bt.Strategy('50MA_crossover',
[bt.algos.WeighTarget(tw),
bt.algos.Rebalance()])
test7 = bt.Backtest(ma_cross, prices_df, integer_positions=False)
res = bt.run(test1, test2, test3, test4, test5, test6, test7)
res.stats
res.display_lookback_returns()
res.plot()
res.plot_security_weights(backtest='EqualRisk')
res.plot_security_weights(backtest='EW')
res.plot_correlation()
res.get_transactions(strategy_name='TechHeavy')