Forex Trading Diary #7 - New Backtest Interface

Forex Trading Diary #7 - New Backtest Interface

Although I've spent the majority of this month researching time series analysis for the upcoming article series, I've also been working on QSForex attempting to improve the API somewhat.

In particular I've made the interface for beginning a new backtest a lot simpler by encapsulating a lot of the "boilerplate" code into a new Backtest class. I've also modified the system to be fully workable with multiple currency pairs. In this article I'll describe the new interface and show the usual Moving Average Crossover example on both GBP/USD and EUR/USD.

New Backtest Interface

I've modified the backtest interface so that instead of having to create a custom backtest.py file as before, you can simply create an instance of Backtest and populate it with your trading components.

The best way to get started with the new approach is to take a look in the examples/ directory and open up mac.py:

from __future__ import print_function

from qsforex.backtest.backtest import Backtest
from qsforex.execution.execution import SimulatedExecution
from qsforex.portfolio.portfolio import Portfolio
from qsforex import settings
from qsforex.strategy.strategy import MovingAverageCrossStrategy
from qsforex.data.price import HistoricCSVPriceHandler


if __name__ == "__main__":
    # Trade on GBP/USD and EUR/USD
    pairs = ["GBPUSD", "EURUSD"]
    
    # Create the strategy parameters for the
    # MovingAverageCrossStrategy
    strategy_params = {
        "short_window": 500, 
        "long_window": 2000
    }
   
    # Create and execute the backtest
    backtest = Backtest(
        pairs, HistoricCSVPriceHandler, 
        MovingAverageCrossStrategy, strategy_params, 
        Portfolio, SimulatedExecution, 
        equity=settings.EQUITY
    )
    backtest.simulate_trading()

As you can see it is relatively short. Firstly the code imports the necessary components, namely the Backtest, SimulatedExecution, Portfolio, MovingAverageCrossStrategy and HistoricCSVPriceHandler.

Secondly, we define the pairs that we'll be trading with and then create a dictionary known as strategy_params. This essentially contains any keyword arguments that we may wish to pass to our strategy. In the case of a Moving Average Crossover we need to pass the rolling window lengths. These values are in terms of "ticks".

Finally we create a Backtest instance and pass all of the objects as parameters. Then, we run the backtest itself.

Within the new backtest.py we call this method:

# backtest.py

    def simulate_trading(self):
        """
        Simulates the backtest and outputs portfolio performance.
        """
        self._run_backtest()
        self._output_performance()
        print("Backtest complete.")

It carries out the backtest calculation (i.e. portfolio updating as ticks come in) as well as computing and outputting the performance in equity.csv.

As before we can still produce a plot of the output with the backtest/output.py script. I'll use this script below when we discuss multiple currency pair implementation.

Multiple Currency Pairs

We're finally at the point where we can test our first non-trivial trading strategy on high-frequency tick data across multiple currency pairs!

To achieve this I've modified how the MovingAverageCrossStrategy is handled. For completeness I've put the full listing below:

class MovingAverageCrossStrategy(object):
    """
    A basic Moving Average Crossover strategy that generates
    two simple moving averages (SMA), with default windows
    of 500 ticks for the short SMA and 2,000 ticks for the
    long SMA.

    The strategy is "long only" in the sense it will only
    open a long position once the short SMA exceeds the long
    SMA. It will close the position (by taking a corresponding
    sell order) when the long SMA recrosses the short SMA.

    The strategy uses a rolling SMA calculation in order to
    increase efficiency by eliminating the need to call two
    full moving average calculations on each tick.
    """
    def __init__(
        self, pairs, events, 
        short_window=500, long_window=2000
    ):
        self.pairs = pairs
        self.pairs_dict = self.create_pairs_dict()
        self.events = events      
        self.short_window = short_window
        self.long_window = long_window

    def create_pairs_dict(self):
        attr_dict = {
            "ticks": 0,
            "invested": False,
            "short_sma": None,
            "long_sma": None
        }
        pairs_dict = {}
        for p in self.pairs:
            pairs_dict[p] = copy.deepcopy(attr_dict)
        return pairs_dict

    def calc_rolling_sma(self, sma_m_1, window, price):
        return ((sma_m_1 * (window - 1)) + price) / window

    def calculate_signals(self, event):
        if event.type == 'TICK':
            pair = event.instrument
            price = event.bid
            pd = self.pairs_dict[pair]
            if pd["ticks"] == 0:
                pd["short_sma"] = price
                pd["long_sma"] = price
            else:
                pd["short_sma"] = self.calc_rolling_sma(
                    pd["short_sma"], self.short_window, price
                )
                pd["long_sma"] = self.calc_rolling_sma(
                    pd["long_sma"], self.long_window, price
                )
            # Only start the strategy when we have created an accurate short window
            if pd["ticks"] > self.short_window:
                if pd["short_sma"] > pd["long_sma"] and not pd["invested"]:
                    signal = SignalEvent(pair, "market", "buy", event.time)
                    self.events.put(signal)
                    pd["invested"] = True
                if pd["short_sma"] < pd["long_sma"] and pd["invested"]:
                    signal = SignalEvent(pair, "market", "sell", event.time)
                    self.events.put(signal)
                    pd["invested"] = False
            pd["ticks"] += 1

Essentially we create an attribute dictionary attr_dict that stores the number of elapsed ticks and whether the strategy is "in" the market for that particular pair.

In calculate_signals we wait for a TickEvent to be received and then calculate the rolling Simple Moving Averages for the short and long windows. Once we exceed the short window for a particular pair, the strategy goes long and exits in the same manner as before, although for each pair separately.

I have made use of 2 months of data for both GBP/USD and EUR/USD and the backtest takes quite some time to execute! It takes around 10-15 minutes on my system, including the calculation of drawdown.

However, once the backtest is complete we're able to use backtest/output.py to produce the following performance chart:

Performance chart of the GBP/USD and EUR/USD MovingAverageCrossover for the period April 2015 - May 2015.

Clearly the performance is not great as the strategy remains almost entirely "underwater" as time goes on. That being said, we shouldn't expect much from such a basic strategy on high-frequency tick data. In the future we are going to be looking at far more sophisticated approaches to trading at this time scale.

Hopefully it will provide a useful starting point for developing more sophisticated strategies. I'm looking forward to seeing what others come up with in the near future!

Next Steps

At this stage there is a list of issues over on Github that need attention. I'm going to be slowly working through those in the next month.

In particular I would like to make the system a lot faster, since it will allow parameter searches to be carried out in a reasonable time. While Python is a great tool, it's one drawback is that it is relatively slow when compared to C/C++. Hence I will be carrying out a lot of profiling to try and improve the execution speed of both the backtest and the performance calculations.

In addition, I've had some comments from people suggesting that they'd like to see more varied order types than the simple Market Order. For carrying out proper HFT strategies against OANDA we are going to need to use Limit Orders. This will probably require a reworking of how the system currently executes trades, but it will allow a much bigger universe of trading strategies to be carried out.

Please get in touch at support@quantstart.com if you have any suggestions, comments or bug reports. I'm always eager to hear how people have ended up using QSForex and what modifications have been made.