Elitequant Backtesting

Introduction

Elitequant is an open source low latency event driven trading system. This post goes over the structure of its historical backtesting module. I’ll use Elitequant Python as example but the code structure is shared literally with other lanaguages such as Elitequant R and Elitequant Matlab.

This post serves as preparation stage for future works in backtesting quantitative systematic strategies. Nevertheless, you can safely skip it without jeopardizing the ability of following future posts if you have already your own backtesting system. Even if you haven’t yet, there are other backtest platforms available such as Quantopian in Python, QuantConnect in C#, or QuantStrat in R. But they all share one feature in common – event driven mechanism.

Event driven system treats every market quotes, every tranactions, every tweeter tweets as an event, and your trading strategy is expected to react to these event streams. QuantStart gives a good summary about the advantages of event-driven system over traditional vectorised approach, so I’ll just add two comments.

  1. This code structure is natural to traders in how they react to market prices and news, and how they interact with risk managers, performance managers, and order execution brokers.
  2. The code can be easily extended to live trading environment. In fact, Quantopian allows you turn-key switch on to live trading. In my opinion, this is the key advantage because writing your strategy again for live trading will introduce more bugs. You are able to unit test all corner cases and fix bugs to eliminate surprises before playing with real money. You can also replay yesterday’s market by feed yesterday’s tick data stream into the system and further observe the strategy’s reaction.

Backtest Structure

Below is the structure of Elitequant backtesting module. I neglected those files for live trading.

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
+-- brokerage
| +-- backtest_brokerage.py
+-- data
| +-- backtest_data_feed_local_single_symbol.py
| +-- bar_event.py
| +-- data_board.py
| +-- tick_event.py
+-- event
| +-- backtest_event_engine.py
| +-- event.py
+-- order
| +-- fill_event.py
| +-- order_event.py
| +-- order_manager.py
| +-- order_event.py
| +-- order_status.py
| +-- order_type.py
+-- performance
| +-- performace_manager.py
+-- position
| +-- portfolio_manager.py
| +-- position.py
+-- strategy
| +-- __init__.py
| +-- mystrategy
| +-- strategy_manager.py
+-- backtest_engine.py
+-- backtest_optimization_engine.py
+-- config_backtest.yml

In the graph event engine is put as the driving force of the system; and traders or your strategies are supported (surrounded) by people such as order managers and performance managers. all these components are wired up in the file backtest_engine.py.

Data Feed

Data feed feeds the data into the system. An example of data is historical price OHLC bars retrieved from local file in BacktestDataFeedLocalSingleSymbol.

1
2
3
4
def subscribe_market_data(self, symbols):
if symbols is not None:
for sym in symbols:
self._retrieve_historical_data(sym) # retrieve historical data

Event

The Event, representing information, is fundamental base class in event-driven system. Example of its child classes are BarEvent, TickEvent, and OrderEvent that dispatched by EventEngine and handled by other EventHandlers.

Event Engine

EventEngine is located at the center of Backtest engine. It has a while loop to dispatch event information to subscribed event handlers based on event types.

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
def run(self):
"""
run backtest
"""
print("Running Backtest...")
while (self._active):
try:
event = self._queue.get(False)
except Empty: # throw good exception
try:
event = self._datafeed.stream_next()
self._queue.put(event)
except:
# stop if not able to retrieve next event
self._active = False
else: # not empty
try:
# call event handlers
if event.event_type in self._handlers:
[handler(event) for handler in self._handlers[event.event_type]]

if self._generalHandlers:
[handler(event) for handler in self._generalHandlers]
except Exception as e:
print("Error {0}".format(str(e.args[0])).encode("utf-8"))

Backtest Brokerage

BacktestBrokerage simulates order-execution brokers in real world, for example Interactive Brokers or ETrade in retail space.

Currently the brokerage receives an order and fills it with market order immediately for free. If you add an order book, it will support more order types such as limit orders and stop orders. If you want to add transaction costs, slippage, commissions, this is also the place.

Portfolio Manager

PortfolioManager class helps you maintain the book and mark-to-market (MTM).

Performance Manager

PerformanceManager keeps daily trades and evaluates the performances. In the end it creates a tearsheet and saves trades and daily positions to out_dir for closer examination.

I have also placed a RiskManager but she currently just gives green light to every trade with no risk control.

Order Manager

OrderManager is the core class of so-called Order Management System (OMS). it keeps and traces your ordes placed, filed, or cancelled.

Data Board

DataBoard is a help class that record most recent price and volume for all symbols.

Strategy Manager

StrategyManger manages all strategies that are derived from StrategyBase class, such as their market subscription and order fills. She knows about them with the help from the codes in mystrategy/init.py, which picks up all strategy classes in that folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# loop over all the files in the path
for root, subdirs, files in os.walk(path):
for name in files:
# by default, all strategies should end with the word 'strategy'
if 'strategy' in name and '.pyc' not in name:
# add module prefix
moduleName = 'source.strategy.mystrategy.' + name.replace('.py', '')
# import module
module = importlib.import_module(moduleName)

# loop through all the objects in the module and look for the one with 'Strategy' keyword
for k in dir(module):
if ('Strategy' in k) and ('Abstract' not in k):
v = module.__getattribute__(k)
strategy_list[k] = v

Backtest Practice

With all the components in place, all you need to do is to write your own strategy class. An example is moving_average_cross_strategy.py. This simple strategy buys on golden cross when short-term moving average crosses long-term moving average from below, and sells on death cross when the former crosses the later from above.

To backtest it, set up the config_backtest.yaml file and run backtest_engine.py. BacktestEngine is the class where all components are wired up together.

To optimize for example the short-term and long-term parameters, set up the candidate parameters in config_backtest.yaml or directly in backtest_optimization_engine.py. BacktestOptimizationEngine takes a grid search approach to loop through all candidate parameters and uses multi-threading to speed up the process. In addition, the tear_sheet option is turned off during batch optimization.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def optimize(config, target_name):
backtest = BacktestEngine(config)
results = backtest.run(tear_sheet=False)

try:
target_value = results.loc[target_name][0]
except KeyError:
target_value = 0
return (config, target_value, results)

for param in params_list:
config_local = config.copy()
config_local['params'] = param
config_local['batch_tag'] = str(batch_token)
res_list.append(pool.apply_async(optimize, (config_local, target_name)))
batch_token = batch_token + 1
pool.close()
pool.join()

You can conveniently set any variable(s) in your strategy as parameter(s) to be optimized. The trick resides in StraegyBase class where it accesses all metadata from your strategy.

1
2
3
4
5
6
7
8
9
10
11
class StrategyBase(metaclass=ABCMeta):
def on_init(self, params_dict=None):
self.initialized = True

# set params
if params_dict is not None:
for key, value in params_dict.items():
try:
self.__setattr__(key, value)
except:
pass

In backtest_optimization_engine, it demos either reading paramater list from config.yaml or setting it directly in code. The advantage of the latter is to allow you construct parameter grids in a loop. The results are sorted by target performance measure. Each backtest is assigned an ID called batch token, which is used to identify its result files in out_dir folder.

1
2
3
Params:{'short_window': 50, 'long_window': 100},Sharpe ratio:0.7771284386050981
Params:{'short_window': 10, 'long_window': 20},Sharpe ratio:0.7374670421689613
Params:{'short_window': 20, 'long_window': 40},Sharpe ratio:0.5593737983561646

To take a closer look, below is the details from one particular parameter.

Happy trading!