【Backtrader】Guidance 详探(三)

795 阅读6分钟

前言

这个系列已经写了两篇了:

终于要完结了,说实话,粗略的走一下 Guidance 基本上收获不大,即使是现在这样稍微细致点的研究,离真正实践还差的远,接下来如何精进,笔者也在探索中,各位同学如果有什么好的建议,欢迎推荐(要免费的啊,程序员学这些东西怎么能花钱呢,多丢人)。

正文

Demo 的代码就不往上贴了,所以以下内容请对照 Quickstart 文档 中的代码同步阅读。

Customizing the Strategy: Parameters

这个 Demo 主要是介绍变量的用法,只要掌握了注入变量的格式(二维元祖)就行了,代码变化也不大,重点如下:


class TestStrategy(bt.Strategy):
    params = (
        ('exitbars', 5),
    )
    
# other code...

        else:
            # Already in the market ... we might sell
            if len(self) >= (self.bar_executed + self.params.exitbars):
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()
                
# other code...

    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=10)

我们可以看到最开始声明了 exitbars 变量,后面用 self.params.exitbars 就可以直接调用了,很好理解。另外,通过文档和笔者实验,直接使用 self.p.exitbars 也可以成功访问变量

Sizer

注意 cerebro.addsizer(bt.sizers.FixedSize, stake=10) 这句,这里还偷偷地加入了设置 sizer 的功能,这里要稍微展开讲一下了。sizer 翻译过来是筛选器的意思,但是笔者觉得在这里可以简单理解为设置买卖多少股的功能

注:当 stake=0 时,只会触发「BUY CREATE」,而不会触发「BUY EXECUTED」。

通过查看 文档,我们可以知道,添加 sizer 有两种方式。

一种是像 Demo 中的一样,直接通过 cerebro.addsizer 添加,这种可以理解为「全局添加」,也就是所有策略都会使用这里面配置的参数。

另一种是为某个策略添加,可以利用 cerebro.addsizer_byidx 的方式,如下:

idx = cerebro.addstrategy(MyStrategy, myparam=myvalue)
cerebro.addsizer_byidx(idx, bt.sizers.SizerFix, stake=5)

也可以在策略中通过 setsizer 主动设置,不过文档中没有对于此用法进行详细说明,笔者实验出了一种用法,仅做参考:

    def next(self):
        self.setsizer(bt.sizers.FixedSize(stake=100))

最后,除了系统自带的一些 Sizer,如 FixedSize、FixedReverser、AllInSizer、PercentSizer 之外,还可以自定义 Sizer。稍微复杂一些,这里就不展开了,总之就是各种设置买卖股数的逻辑了。

Adding an indicator

这个 Demo 更接近实际策略了,增加了「指标」的逻辑,最常用的指标就是「均线」了。我们来看下主要代码:

class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
    )

    def __init__(self):
# other code...
        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)

# other code...
    def next(self):
# other code...
        # Check if we are in the market
        if not self.position:
            # Not yet ... we MIGHT BUY if ...
            if self.dataclose[0] > self.sma[0]:
                # BUY, BUY, BUY!!! (with all possible default parameters)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()
        else:
            if self.dataclose[0] < self.sma[0]:
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()

代码很好理解,收盘价高于 15 日均线就买,低于 15 日均线就卖(果然这么操作是赔钱的)。毫无疑问,Indicatior(指标)是做策略最重要的数据,笔者数了一下一共有 138 个 Indicators,这武器库看起来挺强大的,后面应该会单开 Indicator 相关的系列文章,所以这里还是简单看下用法吧。

其实用法也没多少可说的,就一点可能值得一说。Indicator 的文档中提到了,最好在 __init__ 当中做好数据的计算,而不是在 next 中。就像下例中的代码,在 next 中只是使用 buy_sig 这个变量,所有相关的计算都在 __init__ 当中完成了。

class MyStrategy(bt.Strategy):

    def __init__(self):

        sma1 = btind.SimpleMovingAverage(self.data)
        ema1 = btind.ExponentialMovingAverage()

        close_over_sma = self.data.close > sma1
        close_over_ema = self.data.close > ema1
        sma_ema_diff = sma1 - ema1

        buy_sig = bt.And(close_over_sma, close_over_ema, sma_ema_diff > 0)

    def next(self):

        if buy_sig:
            self.buy()

这样有诸多好处,其中最大的好处就是「快」。听人劝吃饱饭,我们就按官方推荐的做吧。

Visual Inspection: Plotting

这个 Demo 主要是展示「可视化」视图,笔者在《【Backtrader】Guidance 初探》中有提到:要想用此功能需要安装「backtrader 绘图版」,还要重装一下 matplotlib,安装代码如下:

pip install backtrader-plotting
# Fix Bug, see: https://github.com/mementum/backtrader/pull/418
pip uninstall matplotlib && \
pip install matplotlib==3.2.2

然后来看下重点代码:

class TestStrategy(bt.Strategy):
# other code...
    def __init__(self):
# other code...
        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)

        # Indicators for the plotting show
        bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
        bt.indicators.WeightedMovingAverage(self.datas[0], period=25,
                                            subplot=True)
        bt.indicators.StochasticSlow(self.datas[0])
        bt.indicators.MACDHisto(self.datas[0])
        rsi = bt.indicators.RSI(self.datas[0])
        bt.indicators.SmoothedMovingAverage(rsi, period=10)
        bt.indicators.ATR(self.datas[0], plot=False)
        
# other code...

if __name__ == '__main__':
# other code...

    # Plot the result
    cerebro.plot()

显而易见的,最后一句 cerebro.plot() 就是触发可视化的一句,除此之外也没有再跟 plot 有关的代码了。如此看来, __init__ 方法中的那些 bt.indicators.xxx 代码是关键。于是笔者试着将它们都注释了再运行,果然效果是不一样的。具体效果见下图:

原 Demo 代码效果 image.png

只保留 sma 的效果 image.png

现在基本可以确定了,每使用一个 indicator,视图当中就会渲染出来相应的图形。想想也合理,毕竟我们可视化的目的就是要看代码逻辑是否符合预期,展示出来是很合理的。

但是如果我不想展示呢?解决方法也比较简单,每个 indicator 里都会有 plot 这个参数,默认值是 True,设置为 False 就不会展示了。

Let’s Optimize

神器来了,上例中用 15 日均线亏钱,那用其他均线能不能赚钱呢?能不能批量试一下呢?答案是肯定的,这可是 backtrader 的看家本领啊。我们来看一下重点代码:

class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 15),
        ('printlog', False),
    )

    def log(self, txt, dt=None, doprint=False):
        ''' Logging function fot this strategy'''
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))

    def stop(self):
        self.log('(MA Period %2d) Ending Value %.2f' %
                 (self.params.maperiod, self.broker.getvalue()), doprint=True)
# other code...

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    # cerebro.addstrategy(TestStrategy)
    strats = cerebro.optstrategy(
        TestStrategy,
        maperiod=range(10, 31))
        
# other code...

先来看下运行后的结果:

2000-12-29, (MA Period 10) Ending Value 877.50
2000-12-29, (MA Period 11) Ending Value 878.70
2000-12-29, (MA Period 12) Ending Value 839.80
2000-12-29, (MA Period 13) Ending Value 899.90
2000-12-29, (MA Period 14) Ending Value 902.50
2000-12-29, (MA Period 15) Ending Value 975.60
2000-12-29, (MA Period 16) Ending Value 961.90
2000-12-29, (MA Period 17) Ending Value 952.60
2000-12-29, (MA Period 18) Ending Value 1011.00
2000-12-29, (MA Period 19) Ending Value 1039.40
2000-12-29, (MA Period 20) Ending Value 1073.20
2000-12-29, (MA Period 21) Ending Value 1055.10
2000-12-29, (MA Period 22) Ending Value 1057.60
2000-12-29, (MA Period 23) Ending Value 1021.50
2000-12-29, (MA Period 24) Ending Value 1018.80
2000-12-29, (MA Period 25) Ending Value 1012.40
2000-12-29, (MA Period 26) Ending Value 998.30
2000-12-29, (MA Period 27) Ending Value 983.10
2000-12-29, (MA Period 28) Ending Value 976.90
2000-12-29, (MA Period 29) Ending Value 984.20
2000-12-29, (MA Period 30) Ending Value 980.80

诶?之前的那些 log 呢?刚开始笔者也一脸懵逼,好像没怎么改代码啊,log 怎么全没了?仔细一看才发现:log 方法加了一个 if 判断啊,默认就是不打印的,如果传了 doprint=True 才会打印。怪不得只打印了 stop 方法里的 log 呢,因为它传的 doprint=True。没错,这里又出来了一个钩子函数 stop,有了前面两篇的积累,相信这里就不用多解释了。

最后来看看最关键的一句:

    # Add a strategy
    # cerebro.addstrategy(TestStrategy)
    strats = cerebro.optstrategy(
        TestStrategy,
        maperiod=range(10, 31))

注意,这里用 optstrategy 方法代替了 addstrategy,而且传参也变成了数组。结合上面的展示结果,我们也能够比较容易的理解,这是在做批量回测。而且从结果来看,20 日均线盈利的效果是最大的。就这样,我们完成了策略的优化,完美~~~

总结

整个 Guidance 的较深入探索完成了,收获明显大了不少,现在有信心用 backtrader 来自己写策略并且测试了。接下来计划用 backtrader 来测试一些比较有名的交易策略,一是熟悉一下 backtrader 的用法,二是真实检验一下这些策略是不是真的好使。

不过数据从哪来是个难题啊,花钱肯定是弄得到,但是这种事情花钱不就对不起「程序员」这三个字了嘛,应该能淘到的,有信息的同学欢迎留言提供哦~

用百分之三百的努力,把你吹的牛逼实现。