Search code examples
pythonalgorithmic-tradingquantitative-financeback-testingbacktrader

long and short strategy with macd indicator in Backtrader


I just switched from Matlab to python and even newer to the backtrader library for backtestingtrading strategies. My questions might seem obvious.

My problem seems similar to this : https://community.backtrader.com/topic/2857/wanted-exit-long-and-open-short-on-the-same-bar-and-vice-versa

and this : https://community.backtrader.com/topic/2797/self-close-does-not-clear-position The code below is a simple MACD strategy. Here is the code :

# -*- coding: utf-8 -*-
"""

"""

import backtrader as bt
import argparse
import backtrader.feeds as btFeeds
import numpy as np
import yfinance as yf
import pandas as pd
import talib



class SimpleMACDStrat(bt.Strategy):
    
    def __init__(self):
        #Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close
        self.order = None

    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print(f'{dt.isoformat()} {txt}')
        #Print date and close
        
    
    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
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log('LONG EXECUTED, %.2f' % order.executed.price)
                
            elif order.issell():
                self.log('SELL EXECUTED, %.2f' % order.executed.price)

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        # Write down: no pending order
        self.order = None
        

    def next(self):
        self.log("Close: '{0}'" .format(self.data.adj_close[0]))
        print('%f %f %f %f %f %f %f %f %f %f %f %f %f' % (self.data.Indexx[0],self.data.open[0],
                                                 self.data.high[0],self.data.low[0],
                               self.data.close[0],self.data.adj_close[0],
                               self.data.volume[0],self.data.EMA_100[0], 
                               self.data.RSI[0], self.data.CCI[0],
                               self.data.MACD_macd[0],self.data.MACD_sign[0],self.data.MACD_hist[0]))

        if self.order:
            return
        
        if self.data.MACD_hist[0]>0:
            if self.position.size<0 and self.data.MACD_hist[-1]<0  :
                     self.close()
                     self.log('CLOSE SHORT POSITION, %.2f' % self.dataclose[0])
       
            elif self.position.size==0:
                    self.order=self.buy()
                    self.log('OPEN LONG POSITION, %.2f' % self.dataclose[0])
                    
                
        elif self.data.MACD_hist[0]<0:
            if self.position.size>0 and self.data.MACD_hist[-1]>0:
                     self.order=self.close()
                     self.log('CLOSE LONG POSITION, %.2f' % self.dataclose[0])
                            
            elif self.position.size==0:
                    self.order=self.sell()
                    self.log('OPEN SHORT POSITION, %.2f' % self.dataclose[0])
        print('')
        


class BasicIndicatorsFeeded(btFeeds.PandasData):
    lines = ('Indexx', 'adj_close', 'EMA_100', 'RSI', 'CCI', 'MACD_macd', 'MACD_sign', 'MACD_hist',)

    params = ( ('Indexx', 0), ('adj_close', 5), ('volume', 6), 
              ('EMA_100', 7), ('RSI', 8), ('CCI', 9),
              ('MACD_macd', 10), ('MACD_sign', 11), ('MACD_hist', 12),)







if __name__ == '__main__':
    
    cerebro = bt.Cerebro()
    
    #Add data feed to Cerebro
    data1 = yf.download("AAPL",start="2021-08-09", end="2021-12-21",group_by="ticker")
    data1.insert(0,'Indexx',' ')
    data1['Indexx']=range(len(data1))
    data1['EMA_100']=talib.EMA(data1['Adj Close'],100)
    data1['RSI']=talib.RSI(data1['Adj Close'],14)
    data1['CCI']=talib.CCI(data1['High'], data1['Low'], data1['Adj Close'], timeperiod=14)
    data1['MACD_macd']=talib.MACD(data1['Adj Close'], fastperiod=12, slowperiod=26, signalperiod=9)[0]
    data1['MACD_sign']=talib.MACD(data1['Adj Close'], fastperiod=12, slowperiod=26, signalperiod=9)[1]
    data1['MACD_hist']=talib.MACD(data1['Adj Close'], fastperiod=12, slowperiod=26, signalperiod=9)[2]
   # data1['Long_position']
    # Run Cerebro Engine
    cerebro.broker.setcash(8000000000)
    start_portfolio_value = cerebro.broker.getvalue()
    cerebro.addstrategy(SimpleMACDStrat)
    
    data = BasicIndicatorsFeeded(dataname=data1)
    cerebro.adddata(data)
   
    cerebro.run()
    cerebro.plot()
   # print(data1)
    print('-------------------')
    #print('%f' %data)
    # print(data)
    end_portfolio_value = cerebro.broker.getvalue()
    pnl = end_portfolio_value - start_portfolio_value
    print(f'Starting Portfolio Value: {start_portfolio_value:2f}')
    print(f'Final Portfolio Value: {end_portfolio_value:2f}')
    print(f'PnL: {pnl:.2f}')

Here are the results :

results

On 2021-11-10, macd_hist goes from postive to negative. We are expecting that the next day( 2021-11-11):

a)the long position is closed and right after and

b)a short position is opened

1)We see that a) is actually closed the same day. Isn't it supposed to happen the next ?

2)Also a sell is executed the day after, which is not supposed to happen.

Any suggestion for 1) and 2) would be more then welcome. Thanks.

Abbe

EDIT :

Btw, I'm aware the idea can be coded that way (only def next) :

    def next(self):
        #print('%f' % (self.datas[0].Indexxx[0])
        self.log("Close: '{0}'" .format(self.data.adj_close[0]))
        print('%f %f %f %f %f %f %f %f %f %f %f %f %f' % (self.data.Indexx[0],self.data.open[0],
                                                 self.data.high[0],self.data.low[0],
                               self.data.close[0],self.data.adj_close[0],
                               self.data.volume[0],self.data.EMA_100[0], 
                               self.data.RSI[0], self.data.CCI[0],
                               self.data.MACD_macd[0],self.data.MACD_sign[0],self.data.MACD_hist[0]))
        if self.order:
            return
 

        print(self.position)
        if self.data.MACD_hist[0]>0 and self.data.MACD_hist[-1]<0:
                     self.order=self.buy()
                     self.log('CLOSE SHORT POSITION and open long, %.2f' % self.dataclose[0])
                
                
        if self.data.MACD_hist[0]<0 and self.data.MACD_hist[-1]>0:

                     self.order=self.sell()
                     self.log('CLOSE LONG POSITION and open short, %.2f' % self.dataclose[0])
                
        print('')

But I really want to separate the

self.close()

and for instance the

self.buy()

That would allow me later to use different conditions for closing a position and opening one.

Thanks a lot for any inputs, ideas, remarks.

Abbe


Solution

  • In your code you are showing the following:

    if self.data.MACD_hist[0]>0:
                if self.position.size<0 and self.data.MACD_hist[-1]<0  :
                         self.close()
                         self.log('CLOSE SHORT POSITION, %.2f' % self.dataclose[0])
           
                elif self.position.size==0:
                        self.order=self.buy()
                        self.log('OPEN LONG POSITION, %.2f' % self.dataclose[0])
                        
                    
            elif self.data.MACD_hist[0]<0:
                if self.position.size>0 and self.data.MACD_hist[-1]>0:
                         self.order=self.close()
                         self.log('CLOSE LONG POSITION, %.2f' % self.dataclose[0])
                                
                elif self.position.size==0:
                        self.order=self.sell()
                        self.log('OPEN SHORT POSITION, %.2f' % self.dataclose[0])
    

    Based on the logic, it is only possible for one of these conditions to be met in next.

    You are indicating that you would like to have the close and entry separate. You need to change your elif to if. Also, if you are using a criteria of self.position.size == 0, this will not happen until the close is executed, so the bar after the close, not the next one. But if you wish to have other criteria, you could enter it after another if statement.

    if self.data.MACD_hist[0]>0:
                if self.position.size<0 and self.data.MACD_hist[-1]<0  :
                         self.close()
                         self.log('CLOSE SHORT POSITION, %.2f' % self.dataclose[0])
                #### change here  ####
                if SOME OTHER CONDITION:
                        self.order=self.buy()
                        self.log('OPEN LONG POSITION, %.2f' % self.dataclose[0])
                        
                    ...
    

    Once you close the position, it is not really necessary to check if the position goes to 0 units. You can safely assume it will.