Last active
May 19, 2021 09:21
-
-
Save nicoforteza/189fd4bffbc3b66507fe8c97e0ac195d to your computer and use it in GitHub Desktop.
backtest code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pandas as pd | |
import numpy as np | |
# names for the asset selection | |
NAMES = ['6M_Momentum', '1Y_Momentum', '6M_LowVol'] | |
# allocation algorithm names (equally weighted) | |
ALLOCATION = ['EW'] | |
class Backtest: | |
def __init__(self, returns, calendar, strategy, N): | |
"""Main class to perform the computations. | |
Parameters | |
---------- | |
returns: pandas.DataFrame | |
Matrix of returns. There must be no zeros | |
nor Not a Number. | |
calendar: pandas.DatetimeIndex | |
The vector of dates to simulate. | |
The simulation starts one year after! | |
strategy: dict | |
With keys 'name' (stock picking algorithm) | |
and 'distribution' (weight distribution algorithm). | |
If the user would want to run another kind of strategy, he/she should implement it | |
below, in the stock_picking() or asset_allocation() methods. | |
N: int | |
Number of assets in your portfolio | |
""" | |
self.N = N | |
# we need to know the type of the strategy | |
if not isinstance(strategy, dict): | |
raise ValueError("The strategy is a dictionary.") | |
if 'name' not in strategy.keys(): | |
raise ValueError("Specify the strategy name!") | |
if 'distribution' not in strategy.keys(): | |
raise ValueError("Specify the weight distribution algorithm!") | |
if strategy['name'] not in NAMES: | |
raise NotImplementedError("Your stock picking algorithm seems to " | |
"be not implemented yet.") | |
if strategy['distribution'] not in ALLOCATION: | |
raise NotImplementedError("Your weight distribution algorithm seems to " | |
"be not implemented yet.") | |
self.strategy = strategy | |
# we need the returns of the assets | |
if not isinstance(returns, pd.DataFrame): | |
raise ValueError("The passed matrix is not a pandas.DataFrame!") | |
else: | |
if not isinstance(returns.index, pd.DatetimeIndex): | |
raise ValueError("The passed matrix has not the proper index.") | |
self.dates = returns.index | |
self.returns = returns.values | |
self.calendar = calendar | |
self.weights = None | |
self._aux_cum_return = None | |
self.selected = None | |
@property | |
def cumulative_return(self): | |
x = pd.DataFrame(data=self._aux_cum_return, | |
index=self.dates, | |
columns=['Cumulative Return']) | |
x = x.replace(0, np.nan).dropna() | |
return x | |
@staticmethod | |
def evolve_weights(returns, weights): | |
num = weights * (1 + returns) | |
return np.nan_to_num(num / np.sum(num)) | |
def run(self): | |
if self.weights is not None or self._aux_cum_return: | |
raise ValueError("You already simulated!") | |
dates = self.dates | |
# initialize the simulation | |
self.weights = np.zeros((len(dates), self.returns.shape[1])) | |
self._aux_cum_return = np.zeros((len(dates), 1)) | |
# we start iterating! | |
for i, day in enumerate(dates): | |
# we need at least 261 days of history | |
if i > 261: | |
today = i # today's index | |
yty = today - 1 # yesterday's index | |
retornos = self.returns[today] # today's asset returns | |
if today > 0: | |
# evolve the weights of my portfolio | |
self.weights[today, :] = \ | |
self.evolve_weights(retornos, self.weights[yty, :]) | |
# we run our algorithm and do the trades | |
if day in self.calendar: | |
print(str(day)[:-9]) | |
self.stock_picking(today) | |
self.asset_allocation(today) | |
# update the cumulative return of the portfolio | |
if day > dates[0]: | |
# calculate return of the portfolio | |
rent = np.sum(self.weights[yty] * retornos) | |
# accumulate equity | |
self._aux_cum_return[today] = \ | |
(1 + rent) * (1 + self._aux_cum_return[yty]) - 1 | |
def stock_picking(self, today): | |
# prepare the data | |
if self.strategy['name'] == '6M_Momentum': | |
a = self.returns[today - 22 * 6:today, :] | |
elif self.strategy['name'] == '1Y_Momentum': | |
a = self.returns[today - 22 * 12:today, :] | |
elif self.strategy['name'] == '6M_LowVol': | |
a = self.returns[today - 22 * 6:today, :] | |
# do the stock picking | |
if self.strategy['name'] == '6M_Momentum' or \ | |
self.strategy['name'] == '1Y_Momentum': | |
# get the cumulative return of the assets | |
a = np.cumprod(1 + a, 0) - 1 | |
# take the last data point | |
value = a[-1, :] | |
# these are the selected | |
self.selected = \ | |
value > np.percentile(value, (1 - self.N / len(value)) * 100) | |
elif self.strategy['name'] == '6M_LowVol': | |
# calculate the annualised volatility | |
std = np.std(a, 0) * np.sqrt(261) | |
self.selected = std < np.percentile(std, (self.N / len(std)) * 100) | |
def asset_allocation(self, today): | |
# do equally weight | |
if self.strategy['distribution'] == 'EW': | |
self.weights[today] = 0 | |
self.weights[today, self.selected] = 1 / self.N | |
if __name__ == '__main__': | |
# this is the date range we want to simulate | |
dates = pd.bdate_range('20000101', '20180831') | |
# let's generate some random normal returns | |
random_returns = pd.DataFrame(data=np.random.normal(0, 1, size=(len(dates), 500)) / 100, | |
index=dates) | |
# we create the execution dates -- where we trade assets | |
calendar = dates[dates.is_month_start] | |
# what kind of strategy we want? | |
strategy = {'name': '6M_LowVol', | |
'distribution': 'EW'} | |
# construct the class and run the backtest | |
backtest = Backtest(random_returns, calendar, strategy, 50) | |
backtest.run() | |
# plot the result! | |
backtest.cumulative_return.plot() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment