Quantitative Trading and Systematic Investing

Letian Wang Blog on Quant Trading and Portfolio Management

0%

Backtest Trading Strategies using quanttrader

This post shows how to backtest trading strategies in quanttrader python package. It discusses the event-driven backtest framework in general and the code structure of the quanttrader package in specific. It also shows how to do the grid-based parameter search in parallel.

Introduction

quanttrader is a low latency event driven backtest and live trading system in Python. This post goes over its backtesting module. A few years ago I wrote a QuantTrading system in C#. So this one gets suffix two. There are other Python based open-source platforms available such as Quantopian, Backtrader, and PyAlgoTrade. This one tries to be structurely simple. Data loading and strategy evaluations are moved out of the module and what is left is essentially an event engine surrounded by a few supporting functions. Some backtest examples can be found here.

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 quanttrader backtesting module. It neglects 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
+-- brokerage
| +-- backtest_brokerage.py
+-- data
| +-- 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_status.py
| +-- order_type.py
+-- performance
| +-- performace_manager.py
+-- position
| +-- position_manager.py
| +-- position.py
+-- strategy
| +-- __init__.py
| +-- strategy
| +-- strategy_manager.py
+-- backtest_engine.py

In the figure event engine is positioned 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. It is loaded from outside and added into backtest engine.

1
2
data = qt.util.read_ohlcv_csv(datapath)
backtest_engine.add_data('TTT', data)

Event

The Event, representing information, is a 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 with 1bps commission fee and slippage cost, which can be customized. Immediate execution means for example in the case of daily bar, the order is filled at daily close price, which I found better than tomorrow's open price as used in other backtest systems. Nowadays it's feasible to react on a quote at 15:59:59 and then send out an order to be filled at market close, as opposed to waiting overnight to participate in the next day's open auction.

Portfolio Manager

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

Performance Manager

PerformanceManager keeps daily trades and evaluates the performances. It produces an output compatible with pyfolio tearsheet. For the consideration of live trading, quanttrader itself doesn't have dependency on pyfolio.

There is also 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 ordes placed, filled, or cancelled.

Data Board

DataBoard is a help class that record most recent price and volume for all symbols. To keep it simple, backtest data feed doesn't directly feed tick data to strategies; instead strategies are expect to query historical data from data board proactively.

Backtest Practice

With all the components in place, all you need to do is to write your own strategy class. An example is MADoubleCross. 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 optimize for example the short-term and long-term parameters, toggle do_optimize flag to True. To keep quanttrader nimble, the parameter_search is also moved out of the package, because of its dependency on pyfolio.

1
2
3
4
5
6
7
8
9
10
11
12
13
def parameter_search(engine, tag, target_name, return_dict):
"""
This function should be the same for all strategies.
The only reason not included in quanttrader is because of its dependency on pyfolio (to get perf_stats)
"""
ds_equity, _, _ = engine.run()
try:
strat_ret = ds_equity.pct_change().dropna()
perf_stats_strat = pf.timeseries.perf_stats(strat_ret)
target_value = perf_stats_strat.loc[target_name] # first table in tuple
except KeyError:
target_value = 0
return_dict[tag] = target_value
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!

DISCLAIMER: This post is for the purpose of research and backtest only. The author doesn't promise any future profits and doesn't take responsibility for any trading losses.