Potential shortcomings of the approach are the following:
Look-ahead bias
Vectorised backtesting is based on the complete data set available and does not take into account that new data arrives incrementally.
Simplification
For example, fixed transaction costs cannot be modelled by vectorisation, which is mainly based on relative returns. Also, fixed amounts per trade or the non-divisibility of single financial instruments cannot be modelled properly.
Non-recursiveness
Algorithms, embodying trading strategies, might take recurse to state variables over time, like profit and loss up to a certain point in time or similar path-dependent statistics. Vectorisation cannot cope with such features.
Advantages of the event-based backtesting approach generally are the following:
Incremental approach
As in the trading reality, backtesting takes place on the premise that new data arrives incrementally, tick-by-tick and quote-by-quote.
Realistic modelling
One has complete freedom to model those processes that are triggered by a new and specific event.
Path dependency
It is straightforward to keep track of conditional, recursive, or otherwise path-dependent statistics, such as the maximum or minimum price seen so far, and to include them in the trading algorithm.
Reusability
Backtesting different types of trading strategies requires a similar base functionality that can be implemented and unified through object-oriented programming.
Close to trading
Certain elements of an event-based backtesting system can sometimes also be used for the automated implementation of the trading strategy.
Backtesting base class
Retrieving and preparing data
The base class shall take care of the data retrieval and possibly the preparation for the backtesting itself.
Helper and convenience functions
It shall provide a couple of helper and convenience functions that make backtesting easier. Examples are functions for plotting data, printing out state variables, or returning date and price information for a given bar.
Placing orders
The base class shall cover the placing of basic buy and sell orders. For simplicity, only market buy and sell orders are modelled.
Closing out positions
At the end of any backtesting, any market positions need to be closed out. The base class shall take care of this final trade.
The initial amount available is stored twice, both in a private attribute _amount that is kept constant and in a regular attribute amount that represents the running balance. The default assumption is that there are no transaction costs:
def __init__(self, symbol, start, end, amount, ftc=0.0, ptc=0.0, verbose=True):
self.symbol = symbol
self.start = start
self.end = end
self.initial_amount = amount
self.amount = amount
self.ftc = ftc
self.ptc = ptc
self.units = 0
self.position = 0
self.trades = 0
self.verbose = verbose
self.get_data()
The get_data method is called, which retrieves EOD (end of day) data from a CSV for the provided symbol and the given time interval.
def get_data(self):
// Retrieves and prepares the data
raw = pd.read_csv('link/location', index_col=0, parse_dataes=True).dropna()
raw = pd.DataFrame(raw[self.symbol])
raw = raw.loc[self.start:self.end]
raw.rename(columns={self.symbol: 'price'}, inplace=True)
raw['return'] = np.log(raw / raw.shift(1))
self.data = raw.dropna()
The plot_data method is a simple helper method to plot the (adjusted close) values for the provided symbol:
def plot_data(self, cols=None):
// Plots the closing prices for symbol
if cols is None:
cols = ['price']
self.data['price'].plot(figsize=(10, 6), title=self.symbol)
The method get_date_price() returns the date and price information
def get_date_price(self, bar):
// Return date and price for bar
date = str(self.data.index[bar])[:10]
price = self.data.price.iloc[bar]
return date, price
The method print_balance() prints out the current cash balance given a certain bar
def print_balance(self, bar):
// Print out current cash balance info
date, price = self.get_date_price(bar)
print(f'{date} | current balance {self.amount:.2f}')
The method price_net_wealth() does the same for the net wealth (= current balance plus value of trading position)
def print_net_wealth(self, bar):
// Print out current cash balance info
date, price = self.get_date_price(bar)
net_wealth = self.units * price + self.amount
print(f'{date} | current net wealth {net_wealth:.2f}')
Two core methods are place_buy_order() and place_sell_order(), which allow the emulated buying and selling of units of a financial instrument.
def place_buy_order(self, bar, units=None, amount=None):
// Place a buy order
date, price = self.get_date_price(bar)
if units is None:
unites = int(amount / price)
self.amount = -= (units * price) * (1 + self.ptc) + self.ftc
self.units += units
self.trades += 1
if self.verbose:
price(f'{date} | selling {units} units at {price:.2f}')
self.print_balance(bar)
self.print_net_wealth(bar)
def place_sell_order(self, bar, units=None, amount=None):
// Place a sell order
date, price = self.get_date_price(bar)
if units is None:
units = int(amount / price)
self.amount += (units * price) * (1 - slef.ptc) - self.ftc
self.units -= units
self.trades += 1
if self.verbose:
print('f{date} | selling {units} units at {price:.2f}')
self.print_balance(bar)
self.print_net_wealth(bar)
No matter what kind of trading strategy is backtested, the position at the end of the backtesting period needs to be closed out.
def close_out(self, bar):
// Closing out a long or short position
date, price = self.get_date_price(bar)
self.amount += self.units * price
self.units = 0
self.trades += 1
if self.verbose:
print(f'{date} | inventory {self.units} units at {price:.2f}')
print('=' * 55)
print('Final balance [$] {:.2f}'.format(self.amount))
perf = ((self.amount - self.initial_amount) / self.initial_amount * 100)
print('Net Performance [%] {:.2f}'.format(perf))
print('Trades Executed [#] {:.2f}'.format(self.trades))
print('=' * 55)
The final part of the Python script is the __main__ section, which gets executed when the file is run as a script:
if __name__ == '__main__':
bb = BacktestBase("AAPL.0', '2010-1-1', '2019-12-31', 10000)
print(bb.data.info())
print(bb.data.tail())
bb.plot_data()
Long-only backtesting class
The method run_mean_reversion_strategy() implements the backtesting procedure for the mean reversion-based strategy.
def run_mean_reversion_strategy(self, SMA, threshold):
// Backtesting a mean reversion-based strategy
msg = f'\n\nRuning mean reversion strategy | '
msg += f'SMA={SMA} & thr={threshold}'
msg += f'\nfixed costs {self.ftc} | '
msg += f'proportional costs {self.ptc}'
print(msg)
print('=' * 55)
self.position = 0
self.trades = 0
self.amount = self.initial_amount
self.data['SMA'] = self.data['price'].rolling(SMA).mean()
for bar in range(SMA, len(self.data)):
if self.position == 0:
if (self.data['price'].iloc[bar] < self.data['SMA'].iloc[bar] - threshold:
self.place_buy_order(bar, amount=self.amount)
self.position = 1
elif self.position ==1:
if self.data['price'].iloc[bar] >= self.data['SMA'].iloc[bar]:
self.place_sell_order(bar, units==self.units)
self.position = 0
self.close_out(bar)
Long-Short Backtesting Class
In addition to implementing the respective methods for the backtesting of the different strategies, two additional methods to go long and short, go_long() and go_short()
def go_long(self, bar, units=None, amount=None):
if self.position == -1:
self.place_buy_order(bar, units=-self.units)
if units:
self.place_buy_order(bar, units=units)
elif amount:
if amount == 'all':
amount = self.amount
self.place_buy_order(bar, amount=amount)
def go_short(self, bar, units=None, amount=None):
if self.position == 1:
self.place_sell_order(bar, units=self.units)
if units:
self.place_sell_order(bar, units=units)
elif amount:
if amount == 'all':
amount = self.amount
self.place_sell_order(bar, amount=amount)
The core loop from the run_mean_reversion_strategy() method of the BacktestLongShort class. The mean_reversion strategy is picked since the implementation is a bit more involved.
for bar in range(SMA, len(self.data)):
if self.position == 0:
if (self.data['price'].iloc[bar] < self.data['SMA'].iloc[bar] - threshold):
self.go_long(bar, amount=self.initial_amount)
self.position = 1
elif (self.data['price'].iloc[bar] > self.data['SMA'].iloc[bar] + threshold):
self.go_short(bar, amount=self.initial_amount)
self.position = -1
elif self.position == 1:
if self.data['price'].iloc[bar] >= self.data['SMA'].iloc[bar]:
self.place_sell_order(bar, units=self.units)
self.position = 0
elif self.position == -1
if self.data['price'].iloc[bar] <= self.data['SMA'].iloc[bar]:
self.place_buy_order(bar, units=-self.units)
self.position = 0
self.close_out(bar)









