Overview:
In this tutorial, we’re going to be discussing how to build our own backtesting engine using the Numpy and Pandas library. We are then going to backtest a simple sector momentum strategy and plot the performance and weights over time.
Generally, there are two ways to go about building a backtesting engine. The first uses an event-driven framework. In an event-driven framework, a while-loop continuously runs, executing code as new events enter the queue. The benefit of this kind of programming paradigm is that it minimizes look ahead bias and more accurately mimics real life trading. The downside to this framework is the increased complexity of code.
The second way utilizes vectorized code. Vectorized code refers to operations that are performed on multiple components of a vector at the same time. Vectorized backtesters are typically faster and less complex but often lack the realism of event-driven systems.
To keep things simple, we are going to be using vectorized code to test our strategy.
Trading Strategy Rules
The rules of this strategy are simple. Equal weight the top 3 S&P 500 sectors as measured by their 12-month minus 2-month return rebalancing monthly.
Python Code
The first thing we need to do is create a Backtest class and initialize some variables that we will need later on.
class Backtest: def __init__(self, data, calendar , amount = 1000000, window = 0): self.data = data.values self.dates = data.index self.tickers = data.columns self.amount = amount self.window = window self.calendar = calendar
Then we are going to use Python’s built-in @property function to create some functions that will allow us to retrieve our results after they have been simulated. You can read more about the @property function here
@property def cumulative_return(self): x = pd.DataFrame(data=self.cum_return, index=self.dates, columns=['Cumulative Return']) x = x.replace(0, np.nan).dropna() return x @property def get_weights(self): x = pd.DataFrame(data=self.weights[self.window:], index=self.dates[self.window:], columns=self.tickers) return x
Next, we are going to use Pythons static method to create a function that we can call to update our portfolio weights.
@staticmethod def update_weights(returns, weights, dates): num = weights * (1 + returns) return np.nan_to_num(num / np.sum(num))
The last piece of code runs our trading strategy and executes our stock selection model
def run(self): dates = self.dates self.weights = np.zeros((len(dates), self.data.shape[1])) self.cum_return = np.zeros((len(dates), 1)) for i, day in enumerate(dates): today = i # today's index yesterday = today - 1 # yesterday's index if today > self.window: returns = self.data[today] # today's asset returns if np.sum(self.weights[yesterday]) == 0.0: self.cum_return[yesterday] = self.amount # update the weights of my portfolio self.weights[today, :] = self.update_weights(returns, self.weights[yesterday, :], today) # if day is in our rebalance date run our selection model if day in self.calendar: self.stock_selection(today) portfolio_return = np.sum(self.weights[yesterday] * returns) self.cum_return[today] = (1 + portfolio_return) * (self.cum_return[yesterday]) def stock_selection(self, today): a = self.data[today - self.window:today - 2,:] a = np.cumprod(1 + a, 0) - 1 value = a[-1] self.selected = value < np.sort(value)[3] self.weights[today] = 0 self.weights[today, self.selected] = 1 / float(np.sum(self.selected))
Now, all we have to do is instantiate our class and run our “run” method.
Let’s take a look at a performance chart and the weights over time.


So there you have it. Feel free to copy the code below and create your own trading algorithms.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.cbook as cbook
class Backtest:
def __init__(self, data, calendar , amount = 1000000, window = 0):
self.data = data.values
self.dates = data.index
self.tickers = data.columns
self.amount = amount
self.window = window
self.calendar = calendar
@property
def cumulative_return(self):
x = pd.DataFrame(data=self.cum_return, index=self.dates, columns=['Cumulative Return'])
x = x.replace(0, np.nan).dropna()
return x
@property
def get_weights(self):
x = pd.DataFrame(data=self.weights[self.window:],
index=self.dates[self.window:],
columns=self.tickers)
return x
@staticmethod
def update_weights(returns, weights, dates):
num = weights * (1 + returns)
return np.nan_to_num(num / np.sum(num))
def run(self):
dates = self.dates
self.weights = np.zeros((len(dates), self.data.shape[1]))
self.cum_return = np.zeros((len(dates), 1))
for i, day in enumerate(dates):
today = i # today's index
yesterday = today - 1 # yesterday's index
if today > self.window:
returns = self.data[today] # today's asset returns
if np.sum(self.weights[yesterday]) == 0.0:
self.cum_return[yesterday] = self.amount
# update the weights of my portfolio
self.weights[today, :] = self.update_weights(returns, self.weights[yesterday, :], today)
# if day is in our rebalance date run our selection model
if day in self.calendar:
self.stock_selection(today)
portfolio_return = np.sum(self.weights[yesterday] * returns)
self.cum_return[today] = (1 + portfolio_return) * (self.cum_return[yesterday])
def stock_selection(self, today):
a = self.data[today - self.window:today - 2,:]
a = np.cumprod(1 + a, 0) - 1
value = a[-1]
self.selected = value < np.sort(value)[3]
self.weights[today] = 0
self.weights[today, self.selected] = 1 / float(np.sum(self.selected))
strat1 = Backtest(sectors, data.index, window = 12)
strat1.run()
plt.style.use('fivethirtyeight')
fig, ax = plt.subplots(figsize=(20, 10))
ax.plot(strat1.cumulative_return, color = 'crimson')
plt.title('Simple Sector Momentom Strategy')
plt.show()