Reproduction of Research Report: backtrader implements the improved golden fork strategy (with code)

Keywords: Python

Just learning soon, I hope to point out some problems. They are not authoritative and not necessarily correct. Thank you!

1. Data acquisition

Recommended here tushare To obtain transaction data, college students can try for free after registration and approval. Remember to complete the task.

import tushare as ts
import pandas as pd
ts.set_token('your token here') ##Fill in when using for the first time
pro = ts.pro_api()
#Obtain transaction data of China Securities bank
PriceDf = pro.index_daily(ts_code='399986.SZ', start_date = '20050401', end_date = '20200101').drop(['ts_code', 'change', 'pct_chg', 'amount'],axis = 1)
PriceDf = PriceDf.rename(columns = {'trade_date':'datetime'})
PriceDf.to_csv('data.csv')

2. Policy implementation

Improved golden fork strategy

GF Securities improved golden fork strategy Founder Securities simplified and improved golden fork

  The improved golden fork strategy improves the traditional golden fork's choice of closing position and considers the relative change of price, so that the closing position decision can be better than the traditional golden fork most of the time. The difference between founder and gf's improved golden fork is that GF opens an empty order at the dead fork, and others are the same. Here, the data of China Securities bank and founder's strategy are used for back testing.

First, create the indicator class inherited bt from indicator:

The code is very simple, that is, record the signal of each golden fork and whether the last signal is a golden fork or a dead fork, and record the price of the last golden fork for comparison.

import backtrader as bt
import datetime

#GF improved golden fork
class Modified_MAV(bt.Indicator):
    lines = ('sma1','sma2', 'signal','cross_up','cross_down')
    params = (('period1',9),('period2',77))
    def __init__(self):
        self.addminperiod(77)
        #self.pct = bt.indicators.PctChange()
        self.l.sma1 = bt.indicators.SMA(period = self.p.period1)
        self.l.sma2 = bt.indicators.SMA(period = self.p.period2)
        self.l.signal = bt.indicators.CrossOver(self.l.sma1, self.l.sma2)
        self.plotinfo.plotmaster = self.data
    
    def next(self):
        if self.signal[0] == 1:
            #self.line
            self.lines.cross_up[0] = self.data.close[0]
            self.lines.cross_down[0] = self.lines.cross_down[-1]
        elif self.signal[0] == -1:
            self.lines.cross_down[0] = self.data.close[0]
            self.lines.cross_up[0] = self.lines.cross_up[-1]
        else:
            self.lines.cross_up[0] = self.lines.cross_up[-1]
            self.lines.cross_down[0] = self.lines.cross_down[-1]


## Founder improved golden fork (no short)
class Modified_MAV2(bt.Indicator):
    lines = ('sma1','sma2', 'signal','cross_up','last_one')
    params = (('period1',9),('period2',77))
    def __init__(self):
        #self.addminperiod(77)
        self.l.sma1 = bt.indicators.SMA(period = self.p.period1)
        self.l.sma2 = bt.indicators.SMA(period = self.p.period2)
        self.l.signal = bt.indicators.CrossOver(self.l.sma1, self.l.sma2)

    def next(self):
        if self.signal[0] == 1:
            self.lines.last_one[0] = 1
            self.lines.cross_up[0] = self.data.close[0]
        elif self.signal[0] == -1:
            self.lines.last_one[0] = -1
            self.lines.cross_up[0] = self.lines.cross_up[-1]
        else:
            self.lines.cross_up[0] = self.lines.cross_up[-1]
            self.lines.last_one[0] = self.lines.last_one[-1]

Create policy:

class fz_MAV(bt.Strategy):
    
    params = dict(period1 = 9, period2 = 77)

    def log(self, txt, dt=None):
        #'' function of log information '' '
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))
        

    def __init__(self):
        # It is generally used to calculate indicators or pre load data and define variable usage
        self.dataclose = self.data.close
        self.cross = Modified_MAV2(self.data, period1 = self.p.period1, period2 = self.p.period2)
        sma1 = bt.indicators.SMA(period = self.p.period1, plot = False)
        sma2 = bt.indicators.SMA(period = self.p.period2, plot = False)
        self.signal = bt.indicators.CrossOver(sma1, sma2)
        #self.pct = bt.indicators.PctChange()
    
    def prenext(self):
        self.order_target_value(target=self.broker.getvalue())

    def next(self):
        # Get the current size
        size = self.getposition(self.data).size 
        value = self.broker.getvalue()
        # Do more
        if size == 0:
            # open a granary to provide relief
            if self.signal[0] == 1 or (self.dataclose[0] > self.cross.l.cross_up[0] and self.cross.l.last_one[0] > 0):
                self.order = self.order_target_value(target = value, price = self.dataclose[0])
        # Pingduo
        if size>0 and (self.dataclose[0] < self.cross.lines.cross_up[0]):
            self.close(self.data,price= self.dataclose[0])
            
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # order submitted and accepted
            return
        if order.status == order.Rejected:
            self.log(f"order is rejected : order_ref:{order.ref}  order_info:{order.info}")
        if order.status == order.Margin:
            self.log(f"order need more margin : order_ref:{order.ref}  order_info:{order.info}")
        if order.status == order.Cancelled:
            self.log(f"order is concelled : order_ref:{order.ref}  order_info:{order.info}")
        if order.status == order.Partial:
            self.log(f"order is partial : order_ref:{order.ref}  order_info:{order.info}")
        # Check if an order has been completed
        # Attention: broker could reject order if not enougth cash
        if order.status == order.Completed:
            if order.isbuy():
                self.log("buy result : buy_price : {} , buy_cost : {} , commission : {}".format(
                            order.executed.price,order.executed.value,order.executed.comm))
                
            else:  # Sell
                self.log("sell result : sell_price : {} , sell_cost : {} , commission : {}".format(
                            order.executed.price,order.executed.value,order.executed.comm))
    
    def notify_trade(self, trade):
        # Output information at the end of a trade
        if trade.isclosed:
            self.log('closed symbol is : {} , total_profit : {} , net_profit : {}' .format(
                            trade.getdataname(),trade.pnl, trade.pnlcomm))
        if trade.isopen:
            self.log('open symbol is : {} , price : {} ' .format(
                            trade.getdataname(),trade.price))
    
    def stop(self):
        self.log('Ending Value %.2f' %
                (self.broker.getvalue()))

For the code of policy class, focus on the initialization function and next function. For other functions, the great God on the reference station can fine tune according to the needs. It can be seen that with the indicator, the strategy is easy to write. Just judge whether there is a position. If not, open the position if the conditions are met, and close the position if some conditions are met.

3. Back test results

The back test is carried out after the completion of the strategy. As a basic strategy, the reader can modify the code without handling fee and full warehouse. Use quantstats to output icons and results. You can refer to the great God's code. The results are beautiful and direct.

import pyfolio as pf
import quantstats as qs
import matplotlib
matplotlib.use("TKAgg") #If you use Jupiter notebook, you can delete it

cerebro = bt.Cerebro()
data = bt.feeds.GenericCSVData(
        dataname='path/to/your/data.csv',
        fromdate=datetime.datetime(2010, 4, 16),
        todate=datetime.datetime(2016, 6, 13),
        dtformat='%Y-%m-%d',
        datetime=0,
        open=2,
        high=3,
        low=4,
        close=1,
        volume=5,
        openinterest = -1, # -1 means there is no such data
)

#Add data
cerebro.adddata(data)
#No handling fee is set, and the initial capital is 100w
cerebro.broker.setcommission(commission=0)
cerebro.broker.setcash(1000000.0)
#Add analyzer
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate = 0, annualize = False, _name = 'sharpe_ratio')

Add policy
cerebro.addstrategy(MAV, period1 = 9, period2 = 77)

#cerebro.add_order_history((['2005-04-01',100000/1000.872, 1000.872],), notify=True)
#Use quantstats to output the results. Refer to the tutorial in the station
results = cerebro.run()
strat = results[0]
pyfoliozer = strat.analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
returns.index = returns.index.tz_convert(None)
qs.reports.html(returns, output= "webs/stats.html",title =  "" , rf = 0.0)

Strategy result display

The maximum pullback was 33.3%, sharp was 1.07, and the annualized income was 22.19%.

4. Parameter traversal

backtrader supports parameter traversal. Use sns to make a thermodynamic diagram to see the traversal results.

import seaborn as sns
strats = cerebro.optstrategy(fz_MAV,
                        period1 = range(9, 15,1),
                        period2 = range(70, 80,1))

cerebro.broker.setcommission(commission=0)     
cerebro.addanalyzer(bt.analyzers.MyAnnualReturn, _name='MyAnnualReturn')
cerebro.addanalyzer(bt.analyzers.MySharpeRatio, _name='MySharpeRatio')
back = cerebro.run(maxcpus=1)

par_list = [[x[0].params.period1, 
        x[0].params.period2,
        x[0].analyzers.MyAnnualReturn.get_analysis()['annual_return'], 
        x[0].analyzers.MySharpeRatio.get_analysis()['sharpe']
        ] for x in back]

par_df = pd.DataFrame(par_list, columns = ['period1', 'period2', 'annualreturn', 'sharpe'])
par_df_need = par_df[['period1', 'period2', 'sharpe']]
par_df_need = par_df.pivot(index = 'period1', columns= 'period2', values = 'sharpe')
sns.set_context({ "figure.figsize":( 10, 10)}) 
fig = sns.heatmap(data=par_df_need, annot= True ,cmap = 'RdBu_r')
heat_fig = fig.get_figure()

 

 

  Thanks for watching, welcome to point out problems, communicate and make progress together!

 

Posted by m4x3vo on Fri, 17 Sep 2021 13:43:46 -0700