Skip to main content

Event-driven Python backtesting engine - Intro - Part 1

·444 words·3 mins
Anthony Ori
Author
Anthony Ori
~ tinkerer ~

This post is part of a series that will cover how to build an event-driven backtesting engine. I’ve used Python, but you can adapt the concepts to any language.

This project is inspired from this series on Quantstart, with my own additions and refinements. As we’re going over each of the building blocks of the engine, I’ll also show you how to write tests to cover the features.

By the end of this series you should be able to:

  • reproduce something similar in your language of choice, following an event-driven architecture
  • adapt the backtester to a live trading environment or a simulated one with minimal changes

Before diving into the code, here’s a schematic of how events flow in the engine:

flowchart TD

A(Market feed from broker or data provider) --> B(Data management layer)
B--> D(Event Manager)
D-->E(Non-market event)
D-->F(Market event)
F-->G(Process trading signal)
G-->|Trade|H[No]
G-->|Trade|I[Yes]
I-->J(Open new position)
J-->K(Order manager)
K-->L(Calculate optimal position size and risk level)
L-->M(Send order to broker)
M-->N(Book new order into portfolio)
N-->O(Portfolio: monitor positions, calculate risk, calculate PnL)
O-->P(Risk above threshold)
O-->Q(Risk below threshold)
P-->R(Close position)
Q-->S(Keep position open)-->O

In terms of code organisation, I’ve arranged each significant part from the diagram above into its own file, i.e data.py for the data layer, events.py for the events, etc.

For the impatient, the final code looks like this, backtester.py:

Details

# trading_engine/backtester.py

import datetime
import queue

from data import HistoricCsvDataHandler
from strategy import BuyAndHoldStrategy, StatArbStrategy
from portfolio import BuyAndHoldPortfolio
from broker import SimulatedExecutionHandler
from utils import *


if __name__ == "__main__":
    parsed_args = cli_parser()
    asset_class = asset_class_selector(parsed_args)
    data_dir = data_dir_setup(asset_class)

    # main, and only, event queue (FIFO)
    events = queue.Queue()

    # input symbols
    symbols = symbols_filterer(parsed_args.tickers[0], data_dir)

    # engine components
    bars = HistoricCsvDataHandler(events, data_dir, symbols)
    buy_hold_strategy = BuyAndHoldStrategy(bars, events)
    buy_hold_portfolio = BuyAndHoldPortfolio(bars, events, datetime.datetime(1960, 1, 1).strftime("%Y-%m-%d"))
    broker = SimulatedExecutionHandler(events)

    # main execution loop
    while True:
        if bars.continue_backtest:
            bars.update_bars()
        else:
            print("End of backtesting...")
            buy_hold_portfolio.create_equity_curve_dataframe()
            print("\n **** Portfolio statistics **** \n")
            for stat in buy_hold_portfolio.output_summary_stats():
                print(stat)
            break

        while True:
            try:
                event = events.get(False)
            except queue.Empty:
                break
            else:
                if event is not None:
                    if event.type == "MARKET":
                        buy_hold_strategy.calculate_signals(event)
                        buy_hold_portfolio.update_timeindex(event)

                    elif event.type == "SIGNAL":
                        buy_hold_portfolio.update_signal(event)

                    elif event.type == "ORDER":
                        broker.execute_order(event)

                    elif event.type == "FILL":
                        buy_hold_portfolio.update_fill(event)

And you would run it with:

$ python backtester.py --ticker AAPL

to run a backtest on Apple’s stock.

For a portfolio of multiple stocks you can run:

$ python backtester.py --tickers AAPL MSFT AMD INTC

However, there is a benefit to following each part of the series to better understand how it all comes together in the snippet above.

In the next article we will briefly cover at a high level how each parts fits into the whole.


Feel free to share the article on socials: