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


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!