-
Notifications
You must be signed in to change notification settings - Fork 0
/
MeanRevBacktester.py
143 lines (118 loc) · 5.59 KB
/
MeanRevBacktester.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from itertools import product
plt.style.use("seaborn-v0_8")
class MeanRevBacktester():
''' Class for the vectorized backtesting of Bollinger Bands-based trading strategies.
'''
def __init__(self, symbol, SMA, dev, start, end, tc):
'''
Parameters
----------
symbol: str
ticker symbol (instrument) to be backtested
SMA: int
moving window in bars (e.g. days) for SMA
dev: int
distance for Lower/Upper Bands in Standard Deviation units
start: str
start date for data import
end: str
end date for data import
tc: float
proportional transaction/trading costs per trade
'''
self.symbol = symbol
self.SMA = SMA
self.dev = dev
self.start = start
self.end = end
self.tc = tc
self.results = None
self.get_data()
self.prepare_data()
def __repr__(self):
rep = "MeanRevBacktester(symbol = {}, SMA = {}, dev = {}, start = {}, end = {})"
return rep.format(self.symbol, self.SMA, self.dev, self.start, self.end)
def get_data(self):
''' Imports the data from intraday_pairs.csv (source can be changed).
'''
raw = pd.read_csv("intraday_pairs.csv", parse_dates = ["time"], index_col = "time")
raw = raw[self.symbol].to_frame().dropna()
raw = raw.loc[self.start:self.end]
raw.rename(columns={self.symbol: "price"}, inplace=True)
raw["returns"] = np.log(raw / raw.shift(1))
self.data = raw
def prepare_data(self):
'''Prepares the data for strategy backtesting (strategy-specific).
'''
data = self.data.copy()
data["SMA"] = data["price"].rolling(self.SMA).mean()
data["Lower"] = data["SMA"] - data["price"].rolling(self.SMA).std() * self.dev
data["Upper"] = data["SMA"] + data["price"].rolling(self.SMA).std() * self.dev
self.data = data
def set_parameters(self, SMA = None, dev = None):
''' Updates parameters (SMA, dev) and the prepared dataset.
'''
if SMA is not None:
self.SMA = SMA
self.data["SMA"] = self.data["price"].rolling(self.SMA).mean()
self.data["Lower"] = self.data["SMA"] - self.data["price"].rolling(self.SMA).std() * self.dev
self.data["Upper"] = self.data["SMA"] + self.data["price"].rolling(self.SMA).std() * self.dev
if dev is not None:
self.dev = dev
self.data["Lower"] = self.data["SMA"] - self.data["price"].rolling(self.SMA).std() * self.dev
self.data["Upper"] = self.data["SMA"] + self.data["price"].rolling(self.SMA).std() * self.dev
def test_strategy(self):
''' Backtests the Bollinger Bands-based trading strategy.
'''
data = self.data.copy().dropna()
data["distance"] = data.price - data.SMA
data["position"] = np.where(data.price < data.Lower, 1, np.nan)
data["position"] = np.where(data.price > data.Upper, -1, data["position"])
data["position"] = np.where(data.distance * data.distance.shift(1) < 0, 0, data["position"])
data["position"] = data.position.ffill().fillna(0)
data["strategy"] = data.position.shift(1) * data["returns"]
data.dropna(inplace = True)
# determine the number of trades in each bar
data["trades"] = data.position.diff().fillna(0).abs()
# subtract transaction/trading costs from pre-cost return
data.strategy = data.strategy - data.trades * self.tc
data["creturns"] = data["returns"].cumsum().apply(np.exp)
data["cstrategy"] = data["strategy"].cumsum().apply(np.exp)
self.results = data
perf = data["cstrategy"].iloc[-1] # absolute performance of the strategy
outperf = perf - data["creturns"].iloc[-1] # out-/underperformance of strategy
return round(perf, 6), round(outperf, 6)
def plot_results(self):
''' Plots the performance of the trading strategy and compares to "buy and hold".
'''
if self.results is None:
print("Run test_strategy() first.")
else:
title = "{} | SMA = {} | dev = {} | TC = {}".format(self.symbol, self.SMA, self.dev, self.tc)
self.results[["creturns", "cstrategy"]].plot(title=title, figsize=(12, 8))
def optimize_parameters(self, SMA_range, dev_range):
''' Finds the optimal strategy (global maximum) given the Bollinger Bands parameter ranges.
Parameters
----------
SMA_range, dev_range: tuple
tuples of the form (start, end, step size)
'''
combinations = list(product(range(*SMA_range), range(*dev_range)))
# test all combinations
results = []
for comb in combinations:
self.set_parameters(comb[0], comb[1])
results.append(self.test_strategy()[0])
best_perf = np.max(results) # best performance
opt = combinations[np.argmax(results)] # optimal parameters
# run/set the optimal strategy
self.set_parameters(opt[0], opt[1])
self.test_strategy()
# create a df with many results
many_results = pd.DataFrame(data = combinations, columns = ["SMA", "dev"])
many_results["performance"] = results
self.results_overview = many_results
return opt, best_perf