量化交易backtrader实践(二)_基础加强篇(2)_数据进阶实践

打开Notebook,把上一节的代码选运行一遍,这个Notebook里就有了5支股票+2个指数的历史行情数据了,并且确认我们的最简策略已经跑起来了并在Notebook中显示了图形,接着我们就进行后续的实践了!这一节里,我们先来解决数据结构和用法的问题。

如果你发现之前Notebook能显示图形但现在不能显示了,可以参考这篇文章:

Datas与Data

01_向datas添加data

参考文档:Backtrader系列教程②:数据篇_bt.feeds.pandasdata-CSDN博客

首先是self.datas,它就是通过cerebro.adddata()导入的数据表格的集合,以我们现在的5支股票为例,假设这次我们把其中三支股票加了进去,那么datas就是三支股票数据的集合或者list。同理,之前我们只添加1个数据,也就是datas是只有一项的列表。

myStockList = ['001287','002179','600860','300233','002774'] 


#......
from datetime import datetime
import backtrader as bt

class SmaCross(bt.Strategy):             # 先前使用的双均线策略
    params = (('fast', 5),('slow', 10), )

    def __init__(self):
        print(self.data.close)
        self.fast_ma = bt.indicators.SMA(self.data.close, period=self.params.fast)
        self.slow_ma = bt.indicators.SMA(self.data.close, period=self.params.slow)
        self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)

    def next(self):
        if not self.position:
            if self.crossover > 0:       # 金叉买入
                self.buy() 
        else:
            if self.crossover < 0:       # 死叉卖出
                self.close()

添加数据用cerebro.adddata(),这里添加了股票列表中的第2个和第3个以及指数列表中的第1个,

cerebro = bt.Cerebro()                           # 创建Cerebro引擎
# 将数据源设置为PandasData,并加载数据
data = bt.feeds.PandasData(dataname=df_stock_list[1],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data)                     # 将数据添加到Cerebro

data2 = data = bt.feeds.PandasData(dataname=df_stock_list[2],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data2) 

data3 = data = bt.feeds.PandasData(dataname=df_index_list[0],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data3) 


cerebro.addstrategy(SmaCross)                   # 添加策略

cerebro.broker.setcash(100000.0)                # 设置初始资金
cerebro.broker.setcommission(commission=0.001)  # 设置交易佣金
cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')  # 打印起始组合价值

cerebro.run()  # 运行分析

print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')    # 打印最后组合价值
cerebro.plot(style='candle')    # 绘制结果图 

----------------------------------------

运行后输出图形如下,从输出的图形可知:

  1. datas的size = 3(添加了3个数据),而每个data都会在主图上显示
  2. 又均线策略的MA5和MA10只在第1个(下标为[0])的data上显示,其他2个不显示
  3. crossover也只在第1个图的附图上显示,其他2个data不显示
  4. buy/sell的图标只在第1个图上显示,其他2个不显示
  5. 图上没有标注每个data的名字,容易产生混淆

02_给data添加名称(标签)

查看adddata的源码,这里有2个参数,除data外,还有一个参数是name默认=None。

    def adddata(self, data, name=None):
        '''
        Adds a ``Data Feed`` instance to the mix.

        If ``name`` is not None it will be put into ``data._name`` which is
        meant for decoration/plotting purposes.
        '''
        if name is not None:
            data._name = name

前面已经看过bt.feeds.PandasData的源码,处理过来的数据没有name这个属性,因此可以在adddata的时候添加进来。添加的时候可以是股票列表中的字符串,也可以是直接输入的。

cerebro = bt.Cerebro()                     # 创建Cerebro引擎
# 将数据源设置为PandasData,并加载数据
data = bt.feeds.PandasData(dataname=df_stock_list[1],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data,name=myStockList[1])      # 将数据添加到Cerebro
print('data0 name',data._name)

data2 = data = bt.feeds.PandasData(dataname=df_stock_list[2],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data2,name="600860-JCGF") 
print('data2 name',data2._name)

data3 = data = bt.feeds.PandasData(dataname=df_index_list[0],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data3,name='上证指数') 
print('data3 name',data3._name)

cerebro.addstrategy(SmaCross)                    # 添加策略


cerebro.broker.setcash(100000.0)                # 设置初始资金
cerebro.broker.setcommission(commission=0.001)  # 设置交易佣金
cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')  # 打印起始组合价值

cerebro.run()  # 运行分析

print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')    # 打印最后组合价值
cerebro.plot(style='candle')    # 绘制结果图 style='candel'

------------------------
data0 name 002179
data2 name 600860-JCGF
data3 name 上证指数
Starting Portfolio Value: 100000.00
Final Portfolio Value: 76265.94

实践发现,英文和数字代号都可以显示(第1个和第2个),中文字符能添加但在图中只显示乱码为矩形框(第3个)所以要绘图显示的时候尽量不要中文。添加了标签后就不容易把股票顺序搞错了。

03_能否添加2个策略

在策略SmaCross的基础上,快速制作了SmaCross2类,这个类的数据取用的是datas[1]也就是第2个data的(datas[1]==data1),然后再运行看输出图形。

class SmaCross2(bt.Strategy):
    params = (
        ('fast', 5),
        ('slow', 20),
    )

    def __init__(self):
        self.fast_ma1 = bt.indicators.SMA(self.data1.close, period=self.params.fast)
        self.slow_ma1 = bt.indicators.SMA(self.data1.close, period=self.params.slow)

        self.crossover1 = bt.indicators.CrossOver(self.fast_ma1, self.slow_ma1)

    def next(self):
        if not self.position:
            if self.crossover1 > 0:
                self.buy() 
        else:
            if self.crossover1 < 0:
                self.close()
cerebro = bt.Cerebro()                           # 创建Cerebro引擎
# 将数据源设置为PandasData,并加载数据
data = bt.feeds.PandasData(dataname=df_stock_list[1],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data,name=myStockList[1])        # 将数据添加到Cerebro
print('data0 name',data._name)

data2 = data = bt.feeds.PandasData(dataname=df_stock_list[2],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data2,name="600860-JCGF") 
print('data2 name',data2._name)

data3 = data = bt.feeds.PandasData(dataname=df_index_list[0],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data3,name='sh000001') 
print('data3 name',data3._name)

cerebro.addstrategy(SmaCross)                  # 添加策略
cerebro.addstrategy(SmaCross2)                   # 添加策略

cerebro.broker.setcash(100000.0)                # 设置初始资金
cerebro.broker.setcommission(commission=0.001)  # 设置交易佣金
cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')  # 打印起始组合价值

cerebro.run()  # 运行分析

print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')    # 打印最后组合价值
cerebro.plot(style='candle')    # 绘制结果图

------------------------
data0 name 002179
data2 name 600860-JCGF
data3 name sh000001
Starting Portfolio Value: 100000.00
Final Portfolio Value: 84027.31

输出图形如下

从数据和图形输出来看,

  • 首先添加2个策略就会出两次图,很可能是完成策略1的回测后再进行策略2的回测
  • 最后的总资产输出只有一个,是两次回测的最终结果
  • 策略1对应的datas[0],MA5,MA10以及crossover都在主图第1个上,buy/sell也在第1个上
  • 策略2对应的datas[1], MA5、MA20以及crossover1都是主图第2个上,这是正确的
  • 但策略2对应的buy/sell仍然标记在主图第1个上,这是不对的

针对最后一项,在SmaCross2的类里进行一些修改保存为SmaCross2c。首先看buy的源码:

#### 先看源码
    def buy(self, data=None,
            size=None, price=None, plimit=None,
            exectype=None, valid=None, tradeid=0, oco=None,
            trailamount=None, trailpercent=None,
            parent=None, transmit=True,
            **kwargs):
        '''Create a buy (long) order and send it to the broker

          - ``data`` (default: ``None``)

            For which data the order has to be created. If ``None`` then the
            first data in the system, ``self.datas[0] or self.data0`` (aka
            ``self.data``) will be used

从源码中看到 data如果为空则默认使用第1个data即self.datas[0](datas[0]==data0==data),所以代码更改其实非常少,只需要在buy()里面指定data = self.datas[1]即可。

class SmaCross2c(bt.Strategy):
    params = (
        ('fast', 5),
        ('slow', 20),
    )

    def __init__(self):
        self.fast_ma1 = bt.indicators.SMA(self.data1.close, period=self.params.fast)
        self.slow_ma1 = bt.indicators.SMA(self.data1.close, period=self.params.slow)

        self.crossover1 = bt.indicators.CrossOver(self.fast_ma1, self.slow_ma1)

    def next(self):
        if not self.position:
            if self.crossover1 > 0:
                self.buy(data = self.datas[1])      # 更改位置
        else:
            if self.crossover1 < 0:
                self.close(data = self.datas[1])    # 更改位置

然后再次运行,得到的输出图形在第二张图上,则MA5,MA10,Crossover1和buy/sell都会显示在主图的第2个上面了。

小结一下,添加2个策略是可以的,也可以做到策略分别应用于不同的data,最后的结果是两个策略都回测完的最终结果,并且会输出2张图。由这些情况,我觉得在后面的回测中没有必要去一次回测添加2个策略,完全可以做成循环,即一次回测只有一个策略,但可以通过改循环条件让下一次回测再测第二个策略,感觉这样会更加的精简,有效。

组合还是循环

04_一个策略应用于2个data

先回到添加了3个data的位置,现在我们只有一个策略,但当前的需求是每一支股票都用这个策略进行。这就相当于我持有了一个组合,组合中有几支股票,而这个组合使用的策略是相同的,最终的回测结果也是组合的最终收益。

实践需要一步一步的来,3个data当前我们有一点handle不住,因此先从最简单的2个data开始。由能否添加2个策略这一段我们已经知道了假如有2个策略,可以指定其中1个只对datas[1]起作用,所需要注意的就是包括data的数据,由indicators生成的数据以及buy,sell的时候都要指明起作用的datas的下标!所以,也可以把针对2个data的2个策略写到同一个策略里来。

首先,我们用枚举的方式来写代码,在init()中,datas[0]的close就是close0, datas[1]的close就是close1。而在next中,也需要分开来进行处理,例如我们先处理close0的buy/sell,再以同样的方式处理close1的buy/sell,这里再提醒一下,close0的buy/sell默认对应datas[0],括号内无需参数,但close1的buy/sell一定需要在括号内指定参数如self.buy(data = self.datas[1]),否则就又搞到datas[0]的股票上去了,这个就妥妥的张冠李戴。

class DualDataStrategy1(bt.Strategy):
    params = (  ('fast', 5),  ('slow', 20),  )
       
    def __init__(self):
        self.dataclose0 = self.datas[0].close
        self.dataclose1 = self.datas[1].close
        
    def next(self):
        
            if self.getposition(self.datas[0]).size == 0:      # 应用于data0
                if self.dataclose0 < self.dataclose0[-1]:
                    # current close less than previous close
                    if self.dataclose0[-1] < self.dataclose0[-2]:
                        # previous close less than the previous close
                        self.buy(data=self.datas[0])
            else:
                if self.dataclose0 > self.dataclose0[-1]:
                    self.sell(data=self.datas[0])
                    
            if self.getposition(self.datas[1]).size == 0:     # 应用于data1
                if self.dataclose1 < self.dataclose1[-1]:
                    # current close less than previous close
                    if self.dataclose1[-1] < self.dataclose1[-2]:
                        # previous close less than the previous close
                        self.buy(data=self.datas[1])
            else:
                if self.dataclose1 > self.dataclose1[-1]:
                    self.sell(data=self.datas[1])

分别处理这样是可以得到正确的结果的:

可以看到,data0和data1分别使用了这个连跌2天买入,上涨1天卖出的策略,即1个策略用于2个或多个data是可以实现的。但是这个策略实在有点让人有点不能直视(触发buy/sell实在是太多了)。这里也看到用当前的方式写代码,那么如果有10个data我们就得写很长一串,这明显是不合理的;另外,我们对data里的数据做了处理(open,high,low,close,volume),还没有制作指标,指标生成的数据用法是不是也一样呢?

05_一个指标用于2个data

A_linebuffer与indicators异同

那就接着实践一个指标用于2个data。先看一下data里的数据与indicators制作出来的数据有什么区别,从输出结果上看,data里的数据是linebuffer类型,而indicators出来的就是indicators类型。

import backtrader as bt

class DualDataStrategy4(bt.Strategy):
    params = (  ('fast', 5), ('slow', 20),  )
    
    def __init__(self):
        self.dataclose0 = self.datas[0].close
        print(self.dataclose0)
        print(type(self.dataclose0))
        
        self.sma5 = bt.indicators.SMA(self.dataclose0)
        print(self.sma5)
        print(type(self.sma5))    


---------------------------
Starting Portfolio Value: 100000.00
<backtrader.linebuffer.LineBuffer object at 0x000001C89C5E40D0>
<class 'backtrader.linebuffer.LineBuffer'>
<backtrader.indicators.sma.SMA object at 0x000001C89B6D8750>
<class 'backtrader.indicators.sma.SMA'>
Final Portfolio Value: 100000.00

如果我们加上了nex()函数,那么会看到无论是data里的数据和indicators里的数据,它都可以在next里以下标的形式显示出来,就好像DataFrame的两列数据一样。但是,我们也可以从前面的输出的图形看到,indicators出来的是会绘制线条的,而data的数据却不会绘制线条。所以它们既有相同之处,又有差别之处。

import backtrader as bt

class DualDataStrategy4(bt.Strategy):
    params = (  ('fast', 5), ('slow', 20),  )
    
    def __init__(self):
        self.dataclose0 = self.datas[0].close
        print(self.dataclose0)
        print(type(self.dataclose0))
        
        self.sma5 = bt.indicators.SMA(self.dataclose0)
        print(self.sma5)
        print(type(self.sma5))        
        
    def next(self):
        print ('data-close',self.dataclose0[0])
        print ('indicators-sma5',self.sma5[0])

---------------------------
Starting Portfolio Value: 100000.00
<backtrader.linebuffer.LineBuffer object at 0x000001C89CF748D0>
<class 'backtrader.linebuffer.LineBuffer'>
<backtrader.indicators.sma.SMA object at 0x000001C89CF41890>
<class 'backtrader.indicators.sma.SMA'>
data-close 37.71
indicators-sma5 43.823
data-close 38.58
indicators-sma5 43.526666666666664
data-close 37.84
indicators-sma5 43.215
data-close 39.49
indicators-sma5 42.98566666666667
.......
data-close 36.95
indicators-sma5 37.031
Final Portfolio Value: 100000.00

B_indicators的多data数据

经过实践,确认关于多data的应用,indicators与linebuffer是一样的,我们可以从最初级的手动分别给data0和data1设置indicators,然后把2个indicators放到一个list中,也可以在知道有几个data的情况下,用列表推导 for i in range(2)来直接生成list,还可以用 for d in self.datas的形式生成未知个数的所有data的indicators的列表。然后在next()函数中,使用for in enumerate()的方式,

for i, data in enumerate(self.datas):
    # 获取短期和长期移动平均线
    sma_short = self.sma5[i]
    sma_long = self.sma_long[i]

获取到不同datas的indicators,循环对每个data进行buy/sell的计算。这里可以添加log()函数以及notify_trade()函数,将不同datas下的交易都打印出来,以便于理解。

import backtrader as bt

class DualDataStrategy5(bt.Strategy):
    params = ( ('fast', 5), ('slow', 20),  )
    
    def log(self, txt, dt=None):
        ''' Logging function for this strategy'''
        dt = dt or self.datas.datetime.date(0)
        print(f'{dt.isoformat()}, {txt}')
    
    def __init__(self):

        sma5_0 = bt.indicators.SMA(self.data, period=5)
        sma5_1 = bt.indicators.SMA(self.data1, period=5)
        self.sma5 = [sma5_0,sma5_1]                       # 手动处理2个data
        
        # 已知2个data的列表推导
        self.sma_long = [bt.indicators.SMA(self.datas[i].close, period=20) for i in range(2)]
    
        # For in的列表推导       
        self.sma30 = [bt.indicators.SMA(d.close, period=30) for d in self.datas]
    
    def notify_trade(self, trade):   #################记录交易收益情况
        self.ticker = trade.data._name
        if not trade.isclosed:       ### 只要卖单完成,就打印收益
            return
        
        self.log(' 股票%s, 毛利润 %.2f, 净利润 %.2f' %  (self.ticker,trade.pnl, trade.pnlcomm),dt=self.datas[0].datetime.date(0))
    
    def next(self):
        for i, data in enumerate(self.datas):
            # 获取短期和长期移动平均线
            sma_short = self.sma5[i]
            sma_long = self.sma_long[i]
#             sma_long = self.sma30[i]
            
            if self.getposition(data).size == 0:
                if sma_short[0] >sma_long[0] and sma_long[-1]>sma_short[-1]:
                    self.buy(data=data)
            else:
                if sma_short[0] <sma_long[0] and sma_long[-1]<sma_short[-1]:
                    self.sell(data=data)

接着,在cerebro的函数中,只添加2个data运行

cerebro = bt.Cerebro()                           # 创建Cerebro引擎
# 将数据源设置为PandasData,并加载数据
data = bt.feeds.PandasData(dataname=df_stock_list[1],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data,name=myStockList[1])       # 将数据添加到Cerebro

data2 = data = bt.feeds.PandasData(dataname=df_stock_list[2],fromdate=datetime(2021, 11, 1))
cerebro.adddata(data,name=myStockList[2]) 

cerebro.addstrategy(DualDataStrategy5)                  # 添加策略5

cerebro.broker.setcash(100000.0)                # 设置初始资金
cerebro.broker.setcommission(commission=0.001)  # 设置交易佣金
cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')  # 打印起始组合价值

cerebro.run()  # 运行分析

print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')    # 打印最后组合价值
cerebro.plot(style='candle')    # 绘制结果图

------------------------------
Starting Portfolio Value: 100000.00
2022-06-10,  股票600860, 毛利润 554.33, 净利润 503.68
2022-06-23,  股票002179, 毛利润 2867.77, 净利润 2764.90
2022-07-08,  股票600860, 毛利润 -3408.36, 净利润 -3458.36
2022-07-27,  股票002179, 毛利润 -374.86, 净利润 -477.84
2022-08-09,  股票600860, 毛利润 -619.35, 净利润 -665.54
2022-08-22,  股票002179, 毛利润 -1239.89, 净利润 -1287.57
...........
2024-07-23,  股票600860, 毛利润 682.14, 净利润 621.58
2024-08-14,  股票002179, 毛利润 -435.87, 净利润 -465.71
2024-08-20,  股票600860, 毛利润 -197.17, 净利润 -242.99
Final Portfolio Value: 59795.89

由输出结果和图形,确认双均线策略成功用于002179和600860这2支股票,包括SMA5/SMA20的线条显示,以及各自的buy/sell图标显示。另外我们看到,Broker和DataTrade都是以2支股票的组合为整体进行的。 

C_indicators复杂计算多data数据

前面一个实践中,在init()里计算了sma5[ ]和sma20[ ],而放到了next()用所谓的2组数据确定变化的方式判定的金叉和死叉,这个其实在backtrader里有指标Cross系列来解决的,Cross的indicators需要放在init()中进行初始化,而在next()中只需要简单的用>0或<0就能得到金叉或死叉了。由于Cross是对indicators再进行的计算,我们需要实践确认一下会不会有问题。

import backtrader as bt

class DualDataStrategy6(bt.Strategy):
    params = ( ('fast', 5), ('slow', 20),  )
    
    def log(self, txt, dt=None):
        ''' Logging function for this strategy'''
        dt = dt or self.datas.datetime.date(0)
        print(f'{dt.isoformat()}, {txt}')
    
    def __init__(self):

        self.sma5 = [bt.indicators.SMA(d.close,period=5) for d in self.datas]
        self.sma20 = [bt.indicators.SMA(d.close, period=20) for d in self.datas]
        
        self.crossover = [bt.indicators.CrossOver(self.sma5[x], self.sma20[x]) for x in range(len(self.datas))]
    
    def notify_trade(self, trade):   #################记录交易收益情况
        self.ticker = trade.data._name
        if not trade.isclosed:       ### 只要卖单完成,就打印收益
            return
        
        self.log(' 股票%s, 毛利润 %.2f, 净利润 %.2f' %  
                (self.ticker,trade.pnl, trade.pnlcomm),dt=self.datas[0].datetime.date(0))
    
    def next(self):
        for i, data in enumerate(self.datas):
            crossover = self.crossover[i]
            
            if self.getposition(data).size == 0:
                if crossover>0:
                    self.buy(data=data)
            else:
                if crossover<0:
                    self.sell(data=data)

---------------------------------
Starting Portfolio Value: 100000.00
2022-06-10,  股票600860, 毛利润 554.33, 净利润 503.68
2022-06-23,  股票002179, 毛利润 2867.77, 净利润 2764.90
.............
2024-08-14,  股票002179, 毛利润 -435.87, 净利润 -465.71
2024-08-20,  股票600860, 毛利润 -197.17, 净利润 -242.99
Final Portfolio Value: 59795.89

用crossover改写后,运行输出结果与前面一样,但图形输出会多出副图指标crossover。这里我们再回顾一下,Linebuffer的数据默认不绘图,indicators生成的数据默认会绘制图,与股价坐标轴一致的绘制在主图上(例如均线类等),否则绘制在副图(例如crossover,KD,MACD等)。

06_从组合到循环

前面都是在一次回测里(cerebro.run())添加了2个以上的data,当我们制作成几个data都采用同样的策略的时候,就相当于我们的组合采用了某个策略,回测的最终结果是多个股票操作的合力。在这里,我们看到要对多个data一起进行回测,其难度是比较大的,在代码上还需要使用列表推导等把indicators制作成列表,而且在buy/sell的时候必须对应好当前的data,否则就会出现重大错误。所以组合handle起来是比较困难的。

其实大多的时候,我们都是要一支股票一支股票的进行回测的,并不需要用到组合这样的形式,在我目前的认知中,有可能在计算评价指标例如阿尔法-贝塔等需要基准日收益的数据的情况下,可能要把上证指数或沪深300添加进来以外,其他情况下基本上都是1个数据就够用了。

于是我们实践完组合,放弃组合,再来实践循环。

循环,以当前的5支自选股为例,我们一次性把自选股都进行一次回测,然后能得到5组不同的结果,再进一步,当我们有6个经典交易策略的时候,我们把所有的自选股都做6个策略的回测,这时就出来30组不同的结果,统计和对比这30组结果数据,我们就能选出趋势比较好的股票以及对于这支股票比较适合的策略了。

这里就会用到二重循环,当使用循环的时候,cerebro创建对象,添加策略,交易设置等就需要做成一个函数,而不可能再重复的写一大段代码了,而且我们在Notebook中的回测也会越发的轻松起来。

A_制作run_main_plot函数

在前面的实践中,我们一次又一次的复制cerebro.run()的一段代码,然后每次只在里面修改一点点内容,所以可以把它们封装起来,通过参数进行。考虑到后面会制作和优化很多策略,所以策略必须是参数之一,然后回测股票一定是参数,当前需要输出图形那么给上标签也是必须的,所以name1也作为参数。

def run_main_plot_01(strategy1, df_data0,name1):

    cerebro = bt.Cerebro()                         # 创建Cerebro引擎
    
    cerebro.addstrategy(strategy1)                 # 添加策略
    data = bt.feeds.PandasData(dataname=df_data0,fromdate=datetime(2021, 11, 1))
    cerebro.adddata(data,name=name1)       # 将数据添加到Cerebro

    cerebro.broker.setcash(100000.0)                # 设置初始资金
    cerebro.broker.setcommission(commission=0.001)  # 设置交易佣金
    cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
    print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')  # 打印起始组合价值

    cerebro.run()  # 运行分析

    print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')    # 打印最后组合价值
    cerebro.plot(style='candle')    # 绘制结果图



run_main_plot_01(SmaCross,df_stock_list[2], myStockList[2]) # 调用函数

-----------------------
Starting Portfolio Value: 100000.00
Final Portfolio Value: 73220.31
# 图形略

B_自选股列表循环回测

有了run_main_plot函数后,就可以对自选股列表进行循环回测了,当前我们只有一个策略,所以只做1x5的回测,篇幅显示原因,只列出前2个输出和图形。

for i in range(len(df_stock_list)):
    run_main_plot_01(SmaCross,df_stock_list[i], myStockList[i])

---------------------------

init中的数据与显示

07_不同数据在next的使用

前面已经实践过,datas里包含了data的列表,如果adddata()三次,则len(datas) = 3。

接着我们再看data,它是从PandasData过来的,所以它的基本组成也会跟前面制作的DataFrame表差不多,只不过进到backtrader中,它就使用了backtrader的数据结构。然后,原本Pandas很容易调用的索引,行,列就需要用backtrader的方法(不熟悉)来工作了。

人类对于两种非常相近但工作方法有差别的事物总是感觉很恼火,例如我好不容易习惯了打五笔已经都有肌肉记忆了再让我打拼音......但是既然在使用backtrader,那就必须按照人家的规矩来。

先用代码和输出结果进行实践:

from datetime import datetime
import backtrader as bt

class SmaCrossx(bt.Strategy):

    def __init__(self):
        print(self.datas[0])
        print(self.data.close)
        print(self.data.lines.volume)

        self.myClose = self.data.close * 1.0
        print(self.myClose)

        self.fast_ma = bt.indicators.SMA(self.data.close, period=5)
        print(self.fast_ma)

......

----------------
<backtrader.feeds.pandafeed.PandasData object at 0x000001C8A212C810> # data
<backtrader.linebuffer.LineBuffer object at 0x000001C8A2980F10>  # close
<backtrader.linebuffer.LineBuffer object at 0x000001C8AB6EEA50>  # volume
<backtrader.linebuffer.LinesOperation object at 0x000001C8981FAF90>  # myClose
<backtrader.indicators.sma.SMA object at 0x000001C8A99DDE90>     # fast_ma

由输出结果可知,self.datas[0] 或self.data是feeds.pandafeed.PandasData类型,而close,open,high,low,volume以及datetime都是linebuffer类型,它们可以用不同的方式表示(self.data.close == self.data.lines.close == self.data.l.close == self.data.lines[0])。

在后面的绘图进阶里我们会实践在图上画线,使用LinePlotterIndicator方法,但linebuffer类型的数据是不能直接画的,我们可以非常简单的将它们跟1.0相乘就可以画线了,而乘以1.0后它的类型就变成了LineOperation。

另外,由indicators计算出的指标,就是indicators类型。

这些不同的类型在init()里用法以及显示其有效数据都会有着区别,不过它们在next()中用法却是一致的,我们先通过next()来看一下它们所保存的数据:

    def next(self):
        print(bt.num2date( self.data.datetime[0]),
                            'close',self.data.close[0],
                            'myclose',self.myClose[0],
                            'sma5',self.fast_ma[0])

---------------------------
2024-01-24 00:00:00 close 11.27 myclose 11.27 sma5 11.558
2024-01-25 00:00:00 close 11.48 myclose 11.48 sma5 11.398
2024-01-26 00:00:00 close 11.49 myclose 11.49 sma5 11.28
2024-01-29 00:00:00 close 10.78 myclose 10.78 sma5 11.224
2024-01-30 00:00:00 close 10.37 myclose 10.37 sma5 11.078
2024-01-31 00:00:00 close 9.45  myclose 9.45  sma5 10.714
2024-02-01 00:00:00 close 9.08  myclose 9.08  sma5 10.234
......

从这些输出结果上看,虽然它们在backtrader中的类型不一样,但本质上都是对应日期为索引的列数据,就相当于Pandas的Series,或者DataFrame的一列,或者就是一张表格。

由于它们在next()中用法都是一样的,所以在还没有了解清楚init()的情况下,一些策略的判断可以放在next()中进行,比如说close与sma5金叉的判断,在next中就可以分解成2个条件:当天的close大于sma5,但上一天的close要小于sma5,由2个条件组合成了cross的判断。

08_lines数据在init中如何显示

可以先再看下参考文档:Backtrader系列教程②:数据篇_bt.feeds.pandasdata-CSDN博客

小结参考文档,每一个标准data的结构是含有7条line的,它可以使用2种索引(下标和字符串index),所以每条line可以这样获取:

close lowhighopenvolumeopeninterestdatetime
下标self.data.lines[0][1][2][3][4][5][6]
字符串self.data.l.closel.lowl.highopen volumeopeninterestdatetime

因此,在init()中,每条线都是可以直接得到它们的数据的。我们再查看源码并把源码的注释部分交给AI进行解析,得到的回答如下:

这段代码定义了一个名为LineBuffer的类,它继承自LineSingleLineBuffer的设计目的是提供一个接口,以“array.array”(或list)的形式存储数据,其中索引0始终指向当前活跃(用于输入和输出)的项。以下是该类的主要功能和行为的解析:

  1. 正负索引访问

    • 正数索引可以访问过去的值(即数组的左侧)。
    • 负数索引可以访问未来的值(如果数组在右侧进行了扩展)。
  2. 当前值的访问和存储

    • 不需要传递任何索引给需要与当前值交互的实体,因为当前值始终可以通过索引0访问。
    • 当前值的存储也通过索引0进行。
  3. 指针操作

    • 提供了额外的方法来移动指向当前值的指针(如homeforwardextendrewindadvancegetzero),这些操作允许用户在数组中移动或扩展数组。

根据AI的回答,我们再进行代码的实践来确认在init()中lines的数据的表示和显示:

# 为了方便研究,缩短了回测的日期范围
data = bt.feeds.PandasData(dataname=df_data0,
                           fromdate=datetime(2024, 1, 15),
                           todate=datetime(2024,5,11))
# -----------

class SmaCrossx2(bt.Strategy):

    def __init__(self):
        print(self.data.close)
        print(list(self.data.close))
        print(list(self.data.datetime))
        print(bt.num2date(self.data.datetime[0]), self.data.close[0])
        print(bt.num2date(self.data.datetime[-1]), self.data.close[-1])
        print(bt.num2date(self.data.datetime[1]), self.data.close[1])


run_main_plot_01(SmaCrossx2,df_stock_list[2], myStockList[2])

---------------------------------
<backtrader.linebuffer.LineBuffer object at 0x000001C8AD391AD0>
[9.92, 13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 11.49, 10.78, 10.37, 
9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 10.24, 10.35, 
11.39, 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 11.05, 11.12,
 11.49, 11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 11.05, 11.05, 
10.8, 10.46, 10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 9.51, 9.54, 
9.53, 9.61, 10.04, 9.9, 9.99, 10.23, 10.17, 10.14, 9.92]
[739016.0, 738900.0, 738901.0, 738902.0, 738903.0, 738904.0, 738907.0, 738908.0, 
738909.0, 738910.0, 738911.0, 738914.0, 738915.0, 738916.0, 738917.0, 738918.0, 738921.0, 
738922.0, 738923.0, 738924.0, 738935.0, 738936.0, 738937.0, 738938.0, 738939.0, 738942.0, 
738943.0, 738944.0, 738945.0, 738946.0, 738949.0, 738950.0, 738951.0, 738952.0, 738953.0, 
738956.0, 738957.0, 738958.0, 738959.0, 738960.0, 738963.0, 738964.0, 738965.0, 738966.0, 
738967.0, 738970.0, 738971.0, 738972.0, 738973.0, 738974.0, 738977.0, 738978.0, 738979.0, 
738984.0, 738985.0, 738986.0, 738987.0, 738988.0, 738991.0, 738992.0, 738993.0, 738994.0, 
738995.0, 738998.0, 738999.0, 739000.0, 739001.0, 739002.0, 739005.0, 739006.0, 739012.0, 
739013.0, 739014.0, 739015.0, 739016.0]
2024-05-10 00:00:00 9.92
2024-05-09 00:00:00 10.14
2024-01-15 00:00:00 13.62
        
  • linebuffer的显示直接可以用list()方法得到,但注意这个list里第1个数据和最后1个数据相同的
  • 需要把datetime的日期加上一起看,它的第1个和最后1个数据也是相同的
  • 使用 bt.num2date()把数字转成日期类型,739016.0对应的是5-10,这是数据的最后一天
  • datetime[0] 为最后一天的日期, close[0]为最后一天的收盘价 (5-10, 9.92)
  • datetime[-1]为倒数第二天的日期,close[-1]为倒数第二天的收盘价(5-9,10.14)
  • datetime[1]为正数第一天的日期,close[1]为正数第一天收盘价(1-15,13.62)

上面我们用list()强转的方式读取的list数据头和尾是同一个数值,但这其实在LineBuffer对象内部不是这样的数据,所以我们根据源码,找到它使用的迭代器再来看一下它的数据

from datetime import datetime
import backtrader as bt
from itertools import islice

class SmaCrossx3(bt.Strategy):

    def __init__(self):
        print(self.data.close)
        print(list(self.data.close))

        len1 = self.data.buflen()
        print('close data list:',list(islice(self.data.close,0,len1)))
        
        print(bt.num2date(self.data.datetime[0]), self.data.close[0])

-----------------------
<backtrader.linebuffer.LineBuffer object at 0x000001C8AF587150>
close: [9.92, 13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 11.49, 10.78, 10.37,
 9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 10.24, 10.35, 11.39,
 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 11.05, 11.12, 11.49, 
11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 11.05, 11.05, 10.8, 10.46, 
10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 9.51, 9.54, 9.53, 9.61, 
10.04, 9.9, 9.99, 10.23, 10.17, 10.14, 9.92]
iter : [9.92, 13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 
11.49, 10.78, 10.37, 9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 
10.24, 10.35, 11.39, 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 
11.05, 11.12, 11.49, 11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 11.05, 
11.05, 10.8, 10.46, 10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 9.51, 
9.54, 9.53, 9.61, 10.04, 9.9, 9.99, 10.23, 10.17, 10.14]
2024-05-10 00:00:00 9.92

由迭代器出来的数据,最后一天放在了0的位置,这样[-1]和[1]的索引就更容易理解了。

09_get与get_item的用法

get的用法
from datetime import datetime
import backtrader as bt
from itertools import islice

class SmaCrossx3(bt.Strategy):

    def __init__(self):

        len1 = self.data.buflen()
        print('iter :',list(islice(self.data.close,0,len1)))

        get0 = self.data.close.get(ago=0,size=1)
        print('get0',get0)
        get1 = self.data.close.get(ago=1,size=1)
        print("get1",get1)
        get2 = self.data.close.get(ago=-1,size=3)
        print('get2',get2)
        get3 = self.data.close.get(ago=3,size=3)
        print('get3',get3)
        get_len = self.data.close.get(ago=-1,size=len1)
        print(get_len)
        
------------------------
iter : [9.92, 13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 11.49, 10.78,
 10.37, 9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 10.24, 10.35, 
11.39, 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 11.05, 11.12, 
11.49, 11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 11.05, 11.05, 10.8, 
10.46, 10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 9.51, 9.54, 9.53, 
9.61, 10.04, 9.9, 9.99, 10.23, 10.17, 10.14]
get0 array('d')
get1 array('d', [13.62])
get2 array('d', [10.23, 10.17, 10.14])
get3 array('d', [13.62, 13.31, 12.64])
array('d', [13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 11.49, 10.78,
 10.37, 9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 10.24, 
10.35, 11.39, 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 11.05, 
11.12, 11.49, 11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 11.05, 
11.05, 10.8, 10.46, 10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 9.51, 
9.54, 9.53, 9.61, 10.04, 9.9, 9.99, 10.23, 10.17, 10.14])

 由输出结果可知,在init()中,

  • 通过 get() 切片时,从 ago=0 开始取不会返回数据,从其他索引位置开始取能返回数据 
  • ago=+N时,从数据开头数N然后往回取size,此时size不能大于N否则没有数据可取
  • ago=-N时,从数据尾开始数N然后往前取size,size大于N没有问题
  • ago=-1,从数据尾取size即使大于buflen(),也只会得到除第1个数据外的其他数据
其他get相关的用法

查看源码,有好多个与get有关系的方法,我们都拿来做一下

from datetime import datetime
import backtrader as bt
from itertools import islice

class SmaCrossx4(bt.Strategy):

    def __init__(self):

        len1 = self.data.buflen()
        print('iter :',list(islice(self.data.close,0,len1)))
        

        get0 = self.data.close.get(ago=0,size=1)
        print('get0',get0)

        getitem0 = self.data.close.__getitem__(ago=0)
        print('getitem0',getitem0)
        
        getzero1 = self.data.close.getzero(0,5)
        print('getzero1',getzero1)
        
        getzeroval1 = self.data.close.getzeroval(0)
        print('getzeroval1',getzeroval1)
        
        arr1 = self.data.close.array[:]
        print("arr1",arr1)

----------------------
iter : [9.92, 13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 11.49, 10.78,
 10.37, 9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 10.24, 
10.35, 11.39, 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 11.05, 
11.12, 11.49, 11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 11.05, 
11.05, 10.8, 10.46, 10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 9.51, 
9.54, 9.53, 9.61, 10.04, 9.9, 9.99, 10.23, 10.17, 10.14]
get0 array('d')
getitem0 9.92
getzero1 array('d', [13.62, 13.31, 12.64, 12.28, 12.08])
getzeroval1 13.62
arr1 array('d', [13.62, 13.31, 12.64, 12.28, 12.08, 11.06, 11.1, 11.27, 11.48, 11.49, 
10.78, 10.37, 9.45, 9.08, 8.66, 7.79, 7.98, 7.7, 8.47, 8.9, 9.04, 9.22, 9.55, 9.85, 
10.24, 10.35, 11.39, 12.53, 12.61, 11.58, 11.2, 11.25, 11.52, 11.27, 11.24, 11.53, 11.22, 
11.05, 11.12, 11.49, 11.68, 11.83, 12.06, 11.58, 10.86, 10.99, 10.39, 10.71, 10.94, 
11.05, 11.05, 10.8, 10.46, 10.56, 10.2, 10.28, 10.11, 9.68, 8.77, 9.65, 9.5, 9.36, 9.21, 
9.51, 9.54, 9.53, 9.61, 10.04, 9.9, 9.99, 10.23, 10.17, 10.14, 9.92])

 由上可知:

  • get()可获取除最后一天数据(ago=0)外的其他数据
  • getzero()用法与get()是一致的
  • getzeroval()是取某一项的值,但取不到最后一天的值
  • getitem()也是取值,它可以取到ago=0的值
  • 直接使用self.data.close.array[:]能获取到lines中所有的数据,且最后一天(ago=0)数据在最后

最后,有些问题暂时还搞不定,比如说LineOperation的内容在init()中怎么显示出来,虽然去看了源码,self.xx.a是Linebuffer,可以与前面self.data.close一样的取出和显示,但.a只是记录原数据,.b记录的运算的b项(例如2.0),.operation记录的是运算符(例如mul),暂时找不到用哪个变量来记录运算后的数据的。

又比如说indicators怎么迭代显示,在init()中做的SMA5等,目前还搞不定在init()里把数值显示出来。

不过,无论它们是什么,在init()中有多少的区别,我们前面已经做出实验,它们在next()中的用法都是一样的,所以即使暂时搞不定这两项在init()中的显示内容,也不影响我们曲线利用next()来实现我们的需求。

小结

本节从datas开始,实践了添加多个data以及标签,尝试添加了2个策略进行运行(会出现2张图);然后对于1个策略应用于多个data的组合怎么正确工作进行了尝试;接着制作了函数可以在循环中调用以实现多股票的循环回测的功能;最后对Linebuffer,LineOperation以及indicators的数据在next()以及在init()中的使用和显示进行了实践。

通过这些,对于data的理解又进了一层,也为后续的指标、策略打下了较为扎实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值