avatarEsteban Thilliez

Summary

The provided content offers guidance on enhancing the backtesting of trading systems using Python, with a focus on improving logging, setting backtest parameters, implementing trading conditions, utilizing analyzers for performance metrics, and visualizing results.

Abstract

The article delves into the intricacies of backtesting trading strategies with Python, emphasizing the importance of clear logging to understand trading activity. It guides readers through the process of modifying code for better order tracking and introduces the concept of sizers to determine the quantity of assets to trade. The author also discusses the necessity of setting initial cash and implementing conditions to prevent excessive buying. Furthermore, the article explains how to use analyzers, specifically the TradeAnalyzer, to gain insights into trade performance, and it demonstrates how to incorporate transaction costs and slippage for a more realistic backtest. The final section covers the visualization of trading strategies using matplotlib for a graphical representation of trade executions and moving averages.

Opinions

  • The author believes that good logging is crucial for effective backtesting, as poor logging can be useless.
  • It is suggested that visual enhancements, such as color-coding profit and loss in the console output, improve the backtesting experience.
  • The author emphasizes the importance of including transaction costs and slippage in backtests to better simulate real-world trading conditions.
  • The article implies that the TradeAnalyzer is a valuable tool for understanding trade metrics, such as win rate and average profit per trade.
  • The author encourages readers to follow their Medium account and GitHub repository for further learning and access to the full code examples used in the series.

Backtest your Trading Systems with Python — Analysis of the Results

Photo by Carlos Muza on Unsplash

Have you tried running the code we’ve seen previously? (check this if you don’t know what I’m talking about: Backtest your Trading Systems with Python — Strategies Development)

If yes, you should have noticed nothing happens except weird things. It’s because we have to modify our code a little, and we also have to add analyzers to our Cerebro. But one thing at a time, let’s change the code a little first.

But before doing this, you can have a look at the repo I’ve made for this series. It’s on GitHub: Backtrader Series. It should be more convenient for you if you want to check the code while you’re reading this story.

Better logging

When you’re backtesting, it’s important to have good logging. As you’ve seen previously, bad logging is useless. The logging we have now is something like this:

2022-08-22 - 2022-08-22 - 21531.794921875  @ 22163.3245636679
2022-08-23 -  - 1  @ None
2022-08-23 -  - 1  @ None
2022-08-23 - 2022-08-23 - 21600.099609375  @ 22060.92002652373
2022-08-24 -  - 1  @ None
2022-08-24 -  - 1  @ None
2022-08-24 - 2022-08-24 - 21694.58984375  @ 21994.31453874669

Do you understand something? No! I did it on purpose to prove to you that it is important not to neglect this.

First, let’s add something to see when we’re creating a buy order.

self.log('BUY CREATE, %.2f' % self.datas[0].close[0])
# Add this ^
orders = [self.buy()]
self.orders_ref = [order.ref for order in orders if order]

Then, we’ll modify our notify_ordermethod to deal with orders in a better way:

def notify_order(self, order):
    if order.status in [order.Submitted, order.Accepted]:
        # Buy/Sell order submitted/accepted to/by broker - Nothing to do
        return

        # Check if an order has been completed
    if order.status in [order.Completed]:
        if order.isbuy():
            self.log('BUY EXECUTED, %.2f' % order.executed.price)
        elif order.issell():
            self.log('SELL EXECUTED, %.2f' % order.executed.price)

        # Not enough cash: order rejected
    elif order.status in [order.Canceled, order.Margin, order.Rejected]:
        self.log('Order Canceled/Margin/Rejected')
        
        # We remove order if it's useless    
    if not order.alive() and order.ref in self.orders_ref:
        self.orders_ref.remove(order.ref)

We can also delete notify_trade because we won’t need it for now.

Finally, we can modify our self.log in the next method.

self.log('Close: {}, EMA: {}'.format(self.datas[0].close[0], self.ema[0]))

More backtest parameters

Our Cerebro is missing some parameters.

  • Cash: what is our initial cash?
  • Sizer: how many assets should we buy?

For setting the cash, we use cerebro.broker.setcash(cash) . For example: cerebro.broker.setcash(100_000) .

Then, we have to add a sizer. There are many sizers (you can find them all here). We’ll begin with a FixedSize, meaning we will buy the same size for every order.

cerebro.addsizer(bt.sizers.FixedSize, stake=1)

stake is the quantity of assets we buy.

Now let’s run our code again.

One more condition

You should notice something. Sometimes, lots of orders get canceled. It’s because we try to buy assets even if we do have not enough cash because we already bought a lot of assets. To counter this, we’ll add a rule to our strategy. We’ll disable buy operations if we already are in the market.

To know if we are in the market, we use a condition on self.position in our strategy. self.position is > 0 if we’re long and < 0 if we’re short.

So, we can modify our next method:

def next(self):
    self.log('Close: {}, EMA: {}'.format(self.datas[0].close[0], self.ema[0]))

    if not self.position:

        if self.datas[0].close[0] < self.ema[0]:
            self.log('BUY CREATE, %.2f' % self.datas[0].close[0])
            orders = [self.buy()]
            self.orders_ref = [order.ref for order in orders if order]

    if self.datas[0].close[0] > self.ema[0]:
        self.close()

Now run the code, and everything should be fine. We see BUY and SELL operations.

Still some logging improvements

We will now add again notify_trade to display the profit when a trade is closed.

def notify_trade(self, trade):
    if trade.isclosed:
        self.log('PROFIT, %.2f' % (trade.pnl))

Now, I will give you a personal small tip. It’s just a visual effect, but it’s better for backtesting. I like to color what my console displays.

We’ll use the termcolor package.

pip install termcolor

Then, we can for example modify our notify_trade method to color our profit in green if it’s positive or in red if it’s negative.

from termcolor import colored
...
def notify_trade(self, trade):
    if trade.isclosed:
        self.log(colored('PROFIT, %.2f' % (trade.pnl), 'green') if trade.pnl > 0
                 else colored('LOSS, %.2f' % (trade.pnl), 'red'))

colored just takes a string as a first parameter and a color name as a second parameter.

Analyzers

In backtrader, an Analyzer is an object like a Sizer or a Strategy. It’s just something you add to the Cerebro.

There are a lot of analyzers in backtrader. We’ll just see one but you can find the others here.

The one we will see is the TradeAnalyzer . It gives you information about trades, for example, the average profit per trade, the number of winners, the max losers streak, etc…

We first add it to our Cerebro:

cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade')

We provide an optional parameter _name to give a name to our analyzer.

Then, we have to store the result of cerebro.run :

result = cerebro.run()

We can now access our analyzer’s analysis:

strategy_result = result[0]
trade_analyzer = strategy_result.analyzers.trade
analysis = trade_analyzer.get_analysis()
print(analysis)
  1. We run only one strategy, so our strategy’s result is stored in result[0] .
  2. We retrieve our analyzer named trade .
  3. We call get_analysis() to get the analysis from our analyzer. Every analyzer has a get_analysis method.
  4. We print the analysis.

If you run the code, you should see that the analysis is hard to understand. We’ll only keep the metrics interesting us because right now there are too many.

pnl_net_total = analysis.pnl.net.total
pnl_gross_total = analysis.pnl.gross.total
commissions = pnl_gross_total - pnl_net_total

winners = analysis.won.total
losers = analysis.lost.total
win_rate = winners / (winners + losers) * 100

print("---ANALYSIS---")
print("PnL net total: {}".format(pnl_net_total))
print("PnL gross total: {}".format(pnl_gross_total))
print("Commissions: {}".format(commissions))
print("Win rate: {}".format(win_rate)

Adjustments

If you run the code, you will notice something. Commissions are equal to 0. So our backtest is incorrect because when you’re trading for real, there are commissions. Let’s correct this. We’ll also add some slippage.

cerebro.broker.setcommission(commission=0.001) # 0.1%
cerebro.broker.set_slippage_perc(0.001) # 0.1%

Now we can run our code:

---ANALYSIS---
PnL net total: -13241.988396214756
PnL gross total: -11074.33021484366
Commissions: 2167.6581813710964
Win rate: 68.96551724137932

We have commissions, hurra! (we’ve never been so happy to have commissions…) It means now our modelization is closer to reality.

Visual representation

We may want to have a visual aspect of our strategy. We can do it easily with backtrader. First, let’s install matplotlib.

pip install matplotlib

Now we can just plot our strategy:

cerebro.plot()

If you have an error while trying to plot, just remove warnings in the imports of locator.py in matplotlib folder. It’s a known issue happening because backtrader is not up to date with the recent versions of matplotlib.

On the chart, you see when buy and sell orders were executed. You also see the exponential moving average. You see information about trades and about broker.

Final note

Don’t forget to check Medium-backtraderSeries on my GitHub if you want the full code for each story of this series.

The next time, we’ll talk about optimization and how to find the best parameters for your strategies. Be sure to follow me so you won’t miss this story!

Edit: find the other stories of this series here: Improve your Trading with Python

To explore more of my Python stories, click here!

If you liked the story, don’t forget to clap and maybe follow me if you want to explore more of my content :)

You can also subscribe to me via email to be notified every time I publish a new story, just click here!

If you’re not subscribed to medium yet and wish to support me or get access to all my stories, you can use my link:

A Message from InsiderFinance

Thanks for being a part of our community! Before you go:

Python
Algorithmic Trading
Trading
Programming
Finance
Recommended from ReadMedium