000_01Backtrader入门

1、简介

Backtrader的特点,就是两点:

1、易于使用;

2、参见第1条。

那么如何使用Backtrader呢?一共4步:

创建一个Cerebro引擎:

1. 加入一个Strategy。

2. 加载数据。

3. 执行:cerebro.run()。

4. 对执行结果可视化。

就是这么简单!

这么简单如何执行那么多复杂的量化策略,关键就是Backtrader作为一个平台,具有极高的可配置性,也就是可以根据需要进行不同的配置,完成不同的量化策略回测,后续将进行详细描述。

2、安装

一般是通过pip安装:

pip install backtrader

特别注意的是,和backtrader适配的matplotlib版本不能太高,测试可用的版本是3.2.2。如果高于此版本,可以通过如下命令降级:

pip uninstall matplotlib
pip install matplotlib==3.2.2

3、从0到100:一步一步实现一个例子

掌握一项技术最好的方法就是亲手写一个程序,下面采用不断迭代的方法,一步一步,从简单到复杂实现咱们的第一个量化回测程序。

创建第一个程序

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import backtrader as bt

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行下,看看啥结果:

Starting Portfolio Value: 10000.00
Final Portfolio Value: 10000.00

这个程序做了啥?

将Backtrader引入到咱们程序中,命名为bt。

创建了一个机器人大脑(Cerebro),同时隐含创建了一个borker(券商)。

让机器人大脑开始运行。

显示了机器人在券商那里存有多少钱。

等等,为啥只有10000元,失败啊,太少了,没事,咱可以多存点钱到券商。

cerebro = bt.Cerebro()
cerebro.broker.setcash(100000.0)#加到100000元,咱们也富裕了。

再执行看看,果然富裕了:

Starting Portfolio Value: 100000.00
Final Portfolio Value: 100000.00

不对,为啥钱没变啊,这个机器人没给我挣钱啊。因为这个机器人大脑只是初始化,还没发育好,脑子一片空白,啥也做不了,自然挣不了钱。咱们先给机器人点股票数据看看,让他先看看股票数据,说不定能学习点啥。

给空白的大脑加载数据

import backtrader as bt
import pandas as pd
from datetime import datetime
if __name__ == '__main__':
    cerebro = bt.Cerebro()
    #获取数据
    df = pd.read_csv("./datas/day/002624.csv").iloc[:, :6]
    # 处理字段命名,以符合 Backtrader 的要求
    df.columns = [
        'datetime',
        'open',
        'close',
        'high',
        'low',
        'volume',
    ]
    df['openinterest'] = 0.0
    # 把 date 作为日期索引,以符合 Backtrader 的要求
    df.index = pd.to_datetime(df['datetime'])

    start_date = datetime(2022, 9, 30)  # 回测开始时间
    end_date = datetime(2023, 3, 24)  # 回测结束时间
    data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date)  # 加载数据
    cerebro.adddata(data)  # 将数据传入回测系统

    cerebro.broker.setcash(100000.0)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

再执行看看:

Starting Portfolio Value: 100000.00
Final Portfolio Value: 100000.00

大脑开始发育了,起码做到了:

能接受股票的数据了。(这里提供的是下载完美世界(002624)的历史数据作为示例,如何获取数据后续专门讨论)

也能对datetime进行处理,根据需要看具体特定时期的数据了。

但是咱们钱还是没增加了,大脑大脑快快成长吧,快快给俺挣钱啊

给大脑第一个策略

钱有了(在broker券商),股票数据也有了,似乎马上就可以进行有风险的投资了。要投资,就得有一个有投资策略。而投资策略,针对的通常是数据的收盘价(close),也就是根据收盘价决定如何投资。

毕竟股市太凶险,机器人新加的策略是先看看股票的收盘价。

import backtrader as bt
import pandas as pd
from datetime import datetime

# Create a Stratey
class TestStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        ''' 提供记录功能'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 引用到输入数据的close价格
        self.dataclose = self.datas[0].close

    def next(self):
        # 目前的策略就是简单显示下收盘价。
        self.log('Close, %.2f' % self.dataclose[0])

if __name__ == '__main__':
    cerebro = bt.Cerebro()

    # 增加一个策略
    cerebro.addstrategy(TestStrategy)

    df = pd.read_csv("./datas/day/002624.csv").iloc[:, :6]
    # 处理字段命名,以符合 Backtrader 的要求
    df.columns = [
        'datetime',
        'open',
        'close',
        'high',
        'low',
        'volume',
    ]
    df['openinterest'] = 0.0
    # 把 date 作为日期索引,以符合 Backtrader 的要求
    df.index = pd.to_datetime(df['datetime'])
    start_date = datetime(2022, 9, 30)  # 回测开始时间
    end_date = datetime(2023, 3, 24)  # 回测结束时间
    data = bt.feeds.PandasData(dataname=df, fromdate=start_date, todate=end_date)  # 加载数据
    cerebro.adddata(data)  # 将数据传入回测系统

    cerebro.broker.setcash(100000.0)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行后结果:

Starting Portfolio Value: 100000.00   
2022-09-30, Close, 12.42  
2022-10-10, Close, 12.31  
2022-10-11, Close, 12.24  
2022-10-12, Close, 12.67  
...........   (省列N行)
2022-10-13, Close, 12.31   
2023-03-21, Close, 15.92  
2023-03-22, Close, 16.27  
2023-03-23, Close, 16.25  
Final Portfolio Value: 100000.00

看起来还好,没赔钱,股票市场似乎没那么凶险。

关于这个策略(Strategy),需要重点关注:

Strategy初始化的时候,将大脑加载的数据更新到dataclose属性中(注意,这是一个列表,保存股票回测开始时间到结束时间的所有close数据)。 self.datas[0]指向的是大脑通过cerebro.adddata函数加载的第一个数据,本例中指加载浦发银行的股票数据。

self.dataclose = self.datas[0].close指向的是close (收盘价)line

strategy 的next方法针对self.dataclose(也就是收盘价Line)的每一行(也就是Bar)进行处理。在本例中,只是打印了下close的值。next方法是Strategy最重要的的方法,具体策略的实现都在这个函数中,后续还会详细介绍。

这个策略还是没能挣钱,没买卖咋挣钱?大胆点,咱们开始买!

加入买的逻辑到Strategy中

没买卖就没收入,咱们开始买,啥时候买?妈妈说,买东西要便宜,那就如果价格连续降2天,咱就买入,试试看!

import backtrader as bt
import pandas as pd
from datetime import datetime

# 创建一个测试策略
class TestStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        ''' 记录策略信息'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 应用第一个数据源的收盘价
        self.dataclose = self.datas[0].close

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        if self.dataclose[0] < self.dataclose[-1]:
            # 当前的价格比上一次价格(也就是昨天的价格低)

            if self.dataclose[-1] < self.dataclose[-2]:
                # 上一次的价格(昨天)比上上一次的价格(前天的价格)低

                # 开始买!!
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.buy()

执行结果如下:

Starting Portfolio Value: 100000.00  
...(省略n行)  
2023-03-09, Close, 14.27   
2023-03-10, Close, 13.95   
2023-03-10, BUY CREATE, 13.95  
2023-03-13, Close, 14.23   
2023-03-14, Close, 14.22   
2023-03-15, Close, 14.13   
2023-03-15, BUY CREATE, 14.13  
2023-03-16, Close, 14.00 
2023-03-16, BUY CREATE, 14.00  
2023-03-17, Close, 14.77   
2023-03-20, Close, 15.39  
2023-03-21, Close, 15.92  
2023-03-22, Close, 16.27  
2023-03-23, Close, 16.25  
Final Portfolio Value: 100074.68

挣钱了耶,居然挣了74多块钱。从打印可以看出,在收盘价连续2天价格下跌的时候,strategy就执行买入。虽然咱们挣了钱,订单(order)好像创建了,但是不知道是否执行了以及何时以什么价格执行了。后面我们会展示如何监听订单执行的状态。

也许你要问了,咱们买了多少股票(称之为资产asset)?买了啥股票?订单是咋执行的?后续会回答这些问题,在当前示例中:

  • self.datas[0] 就是我们购买了的股票。本例中没有输入其他数据,如果输入了其他数据,购买的股票就不一定是啥了,这个要看具体的策略执行情况。

  • 买了多少股本(stake)的股票?这个通过机器人大脑的position sizer属性来记录,缺省值为1,就是缺省咱们每一次操作只买卖1股。

  • 当前order执行的时候,采用的价格是第二天的开盘价。

  • 当前order执行的时候,没有收佣金。佣金如何设置后续还会说明。

不仅要买,还要卖

一次完整的交易,不仅要买,还要卖。啥时候卖?咱们简单点,就是处理了5个bar数据之后。值得注意的是,这里使用了bar这个概念,没有包含任何时间的概念,也就是一个bar,可以是1分钟,1个小时,也可以是一天,一个月,这些基于你输入的数据,如果你输入的股票每小时(分时)数据,那么一个bar就是一分钟,如果提供是周K数据,一个bar就是一周。本例中,我们获取的数据基于每天,那么一个bar就是一天。

特别之一,这里代码使用了len这个函数,在python中,len通常返回的一个列表中数据的多少,而在backtrader中,重写了len函数,返回的是已经处理过数据行(也就是Bar)。

注意,如果我们不在市场内,就不能卖。什么叫不在市场内,就是你不拥有任何股票头寸,也就是没有买入资产。在策略中,通过 position 属性来记录。

本次代码中,我们将要增加如下:

  • 访问postion获取是否在市场内

  • 会创建买和卖的订单(order)

  • 订单状态的改变会通过notify方法通知到strategy。

# Create a Stratey
class TestStrategy(bt.Strategy):

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

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

        # To keep track of pending orders
        self.order = None

    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('BUY 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):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # 检查是否在市场
        if not self.position:

            # 不在,那么连续3天价格下跌就买点
            if self.dataclose[0] < self.dataclose[-1]:
                    # 当前价格比上一次低

                    if self.dataclose[-1] < self.dataclose[-2]:
                        # 上一次的价格比上上次低

                        # 买入!!! 
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])

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

        else:

            # 已经在市场,5天后就卖掉。
            if len(self) >= (self.bar_executed + 5):#这里注意,Len(self)返回的是当前执行的bar数量,每次next会加1.而Self.bar_executed记录的最后一次交易执行时的bar位置。
                # 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()

执行结果:

Starting Portfolio Value: 100000.00  
...(省略n行)  
2023-03-14, Close, 14.22
2023-03-14, SELL CREATE, 14.22
2023-03-15, SELL EXECUTED, 14.30
2023-03-15, Close, 14.13
2023-03-15, BUY CREATE, 14.13
2023-03-16, BUY EXECUTED, 14.08
2023-03-16, Close, 14.00
2023-03-17, Close, 14.77
2023-03-20, Close, 15.39
2023-03-21, Close, 15.92
2023-03-22, Close, 16.27
2023-03-23, Close, 16.25
2023-03-23, SELL CREATE, 16.25
Final Portfolio Value: 100003.94


从结果看,买了股票5天就卖了。啥,只赚了3.94元钱,会买的是徒弟,会卖的才是师父,看来真的不能瞎卖啊!

券商说,俺的钱呢?

券商说,你这里又买又卖的,我的佣金在哪儿?好的,咱们加上佣金,就0.1%吧,一行代码的事情:

# 0.1% ... 除以100去掉%号。
cerebro.broker.setcommission(commission=0.001)

下面我们看看佣金对收益的影响。

import backtrader as bt
import pandas as pd
from datetime import datetime


# 创建一个测试策略
class TestStrategy(bt.Strategy):

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

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

        # To keep track of pending orders
        self.order = None

    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
    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(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

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

        self.order = None

    def notify_trade(self, trade):  # 交易执行后,在这里处理
        if not trade.isclosed:
            return
        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))  # 记录下盈利数据。



    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # 检查是否在市场
        if not self.position:

            # 不在,那么连续3天价格下跌就买点
            if self.dataclose[0] < self.dataclose[-1]:
                # 当前价格比上一次低

                if self.dataclose[-1] < self.dataclose[-2]:
                    # 上一次的价格比上上次低

                    # 买入!!!
                    self.log('BUY CREATE, %.2f' % self.dataclose[0])

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

        else:

            # 已经在市场,5天后就卖掉。
            if len(self) >= (
                    self.bar_executed + 5):  # 这里注意,Len(self)返回的是当前执行的bar数量,每次next会加1.而Self.bar_executed记录的最后一次交易执行时的bar位置。
                # 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()

if __name__ == '__main__':
    cerebro = bt.Cerebro()

    # 增加一个策略
    cerebro.addstrategy(TestStrategy)

    df = pd.read_csv("./datas/day/002624.csv").iloc[:, :6]
    # 处理字段命名,以符合 Backtrader 的要求
    df.columns = [
        'datetime',
        'open',
        'close',
        'high',
        'low',
        'volume',
    ]
    df['openinterest'] = 0.0
    # 把 date 作为日期索引,以符合 Backtrader 的要求
    df.index = pd.to_datetime(df['datetime'])
    start_date = datetime(2022, 9, 30)  # 回测开始时间
    end_date = datetime(2023, 3, 24)  # 回测结束时间
    data = bt.feeds.PandasData(dataname=df, fromdate=start_date, todate=end_date)  # 加载数据
    cerebro.adddata(data)  # 将数据传入回测系统

    cerebro.broker.setcash(100000.0)
    # 设置佣金0.1 % ...除以100去掉 % 号。
    cerebro.broker.setcommission(commission=0.001)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

结果如下:

Starting Portfolio Value: 100000.00
...(省略n行)
2023-03-14, Close, 14.22
2023-03-14, SELL CREATE, 14.22
2023-03-15, SELL EXECUTED, Price: 14.30, Cost: 14.70, Comm 0.01
2023-03-15, OPERATION PROFIT, GROSS -0.40, NET -0.43
2023-03-15, Close, 14.13
2023-03-15, BUY CREATE, 14.13
2023-03-16, BUY EXECUTED, Price: 14.08, Cost: 14.08, Comm 0.01
2023-03-16, Close, 14.00
2023-03-17, Close, 14.77
2023-03-20, Close, 15.39
2023-03-21, Close, 15.92
2023-03-22, Close, 16.27
2023-03-23, Close, 16.25
2023-03-23, SELL CREATE, 16.25
Final Portfolio Value: 99997.68

晕菜,亏了,挣的钱都给券商了,所以啊,一定不要频繁交易。

同时,我们注意到,每次订单执行,都有一个操作盈利记录(Gross:毛利;Net:净利):

2023-03-15, OPERATION PROFIT, GROSS -0.40, NET -0.43

如何给策略Strategy传递参数

策略这个东西,是高度可配置的,大脑想调整下策略,可以通过配置参数来设定,免得大量硬编码数据,后续难以修改。比如策略之前是过5天就卖,现在想改为过7天再卖,通过修改参数就可以做到。参数如何传递呢?也很简单:

params = (('myparam', 27), ('exitbars', 5),)

就是典型的python元组数据。

我们在创建策略的时候,大脑通过如下方法传递给策略实例:

# 加一个策略
cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)

那么卖出的 逻辑可以修改为:

if len(self) >= (self.bar_executed + self.params.exitbars):

之前我们每次只买1股,太少了,咱们中国一手最少100股,那咋整?可以通过如下代码设置(先设置1000股试试看):

cerebro.addsizer(bt.sizers.FixedSize, stake=1000)


修改这两处之后,咱们再执行下看看(这个修改较简单,就不贴代码了)。

Starting Portfolio Value: 100000.00
...(省略n行)
2023-03-09, Close, 14.27
2023-03-09, SELL CREATE, 14.27
2023-03-10, SELL EXECUTED, Price: 14.11, Cost: 14690.00, Comm 14.11
2023-03-10, OPERATION PROFIT, GROSS -580.00, NET -608.80
2023-03-10, Close, 13.95
2023-03-10, BUY CREATE, 13.95
2023-03-13, BUY EXECUTED, Price: 13.90, Cost: 13900.00, Comm 13.90
2023-03-13, Close, 14.23
...(省略n行)

看如下,每次买卖1000股。

2023-03-10, SELL EXECUTED, Price: 14.11, Cost: 14690.00, Comm 14.11
2023-03-13, BUY EXECUTED, Price: 13.90, Cost: 13900.00, Comm 13.90

加一个指标(indicator)

大家投资的时候,经常听说这指标那指标的,没听说?那均线总听说过吧?咱们加个均线指标来指导我们的策略,毕竟连续两天下跌这个指标太土了,咱们要使用高大上的移动均线指标:

如果当前价格大于均线,就买买买。

如果低于均线,就卖卖卖。怎么听起来像追涨杀跌啊,看起来咱们的机器人像个韭菜。

为了简单起见,同时只能存在一次买卖。

修改也不大,在策略(Strategy)的初始化的时候,引用一个简单的移动平均指标,如下代码:

self.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod)

Strategy代码如下,同时主程序暂时将佣金设置为0. 为了减少数据,对比查看sma的数据,说明一个重要的概念,暂时将回测时间修改为2022-9-1日。

import backtrader as bt
import pandas as pd
from datetime import datetime


# 创建一个测试策略
# Create a Stratey
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 20),
    )

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

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

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

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

    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(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

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

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # Check if we are in the market
        if not self.position:

            # 大于均线就买
            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]:
                # 小于均线卖卖卖!
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

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


if __name__ == '__main__':
    cerebro = bt.Cerebro()

    # 增加一个策略
    cerebro.addstrategy(TestStrategy)
    # cerebro.addstrategy(TestStrategy, myparam=20, exitbars=10)

    df = pd.read_csv("./datas/day/002624.csv").iloc[:, :6]
    # 处理字段命名,以符合 Backtrader 的要求
    df.columns = [
        'datetime',
        'open',
        'close',
        'high',
        'low',
        'volume',
    ]
    df['openinterest'] = 0.0
    # 把 date 作为日期索引,以符合 Backtrader 的要求
    df.index = pd.to_datetime(df['datetime'])
    start_date = datetime(2022, 9, 1)  # 回测开始时间
    end_date = datetime(2023, 3, 24)  # 回测结束时间
    data = bt.feeds.PandasData(dataname=df, fromdate=start_date, todate=end_date)  # 加载数据
    cerebro.adddata(data)  # 将数据传入回测系统

    cerebro.broker.setcash(100000.0)
    # 设置佣金0.1 % ...除以100去掉 % 号。
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

执行下,看看:

Starting Portfolio Value: 100000.00
2022-09-29, Close, 12.47
2022-09-30, Close, 12.42
2022-10-10, Close, 12.31
...(省略n行)
2023-03-17, Close, 14.77
2023-03-17, BUY CREATE, 14.77
2023-03-20, BUY EXECUTED, Price: 15.39, Cost: 15390.00, Comm 15.39
2023-03-20, Close, 15.39
2023-03-21, Close, 15.92
2023-03-22, Close, 16.27
2023-03-23, Close, 16.25
Final Portfolio Value: 99961.69

哎,亏了,还不如追涨杀跌呢!不过,咱们是学习如何使用Backtrader来验证策略,重要是学习,亏就亏了吧。

眼尖的你可能看到了,咱们回测时间是2021-9-1日,为啥系统从2020-9-29开始测试?

这是因为sma(移动平均)的参数为20,也就是要20bar(本例中一个bar就是一天)的数据才能计算,1到19天没数据,可以看下sma的数据:

从第20天开始(20个工作日之后就是9月29日)才有数据,而strategy的next在所有line(本例中,有两个line,一个是open价格,一个是sma)都有有效的时候,才会进行策略的处理。这个例子中,只有一个指标(indicator),实际上可以加随便多少个,来实现复杂的策略,后续再详细讨论。

另外,注意一点,backtrader中对数据的处理是向下取两位小数。

可视化-画图

前面执行程序,每一步都输出文字描述,这个看起来实在不友好,于是Backtrader提供了画图功能。

画图使用的是matplotlib库,前面描述过,必须安装特定版本,不然就有问题。

实现画图,也很简单,加一行代码:

cerebro.plot()

放到cerebro.run()之后就可以了,这里不贴代码了,执行试试看:

2023-03-24%20135922.png

神奇不?一张图可以看出所有:

  • 收盘价和移动平均线。

  • 盈利图;

  • 买卖点;

  • 买卖盈利还是亏损。

  • 成交量。

你以为只能这么点,为了展示咱们强大的画图能力,再加点指标来自动画图显示:

  • 指数移动平均线

  • 权重移动平均线

  • 随机指标

  • MACD

  • RSI

  • RSI的简单移动平均

  • ATR。这个不画图,展示如何控制Line是否参与画图。

这些指标先自行百度,以后涉及到咱们的策略的时候再介绍。

实现的时候,就是在Strategy初始化的时候,加上如下代码:

# 新加指标用于画图
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

2023-03-24%20140850.png

再来点优化

也许你要问了,为啥咱们用20天的移动平均呢?用15天行不行?其实对于同的股票,这个值还可能不同。backtrader的神奇来了,咱们可以对策略进行对比。

具体的实现方法:

首先在大脑(cerebro)调用addstrategy的时候只传入一个策略,而是调用optstrategy传入多个参数。参见如下主代码,策略不变:

其次,在Strategy增加stop方法,该方法在每个strategy完成之后调用,也就是在所有数据处理完成之后再调用。该函数会打印每次策略执行后最终还剩多少钱。

完整代码:

import backtrader as bt
import pandas as pd
from datetime import datetime


# 创建一个测试策略
# Create a Stratey
class TestStrategy(bt.Strategy):
    params = (
        ('maperiod', 20),
        ('printlog', False),
    )

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

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

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None

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

        # 新加指标用于画图
        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

    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(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

            self.bar_executed = len(self)

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

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])

        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return

        # Check if we are in the market
        if not self.position:

            # 大于均线就买
            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]:
                # 小于均线卖卖卖!
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

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


    def stop(self):
        """
        回测结束后输出结果
        """
        self.log("(MA均线: %2d日) 期末总资金 %.2f" % (self.params.maperiod, self.broker.getvalue()), do_print=True)


if __name__ == '__main__':
    cerebro = bt.Cerebro()

    # 增加一个策略
    # cerebro.addstrategy(TestStrategy)
    # cerebro.addstrategy(TestStrategy, myparam=20, exitbars=10)

    # 增加多参数的策略
    strats = cerebro.optstrategy(TestStrategy, maperiod=range(5, 31, 5))

    df = pd.read_csv("./datas/day/002624.csv").iloc[:, :6]
    # 处理字段命名,以符合 Backtrader 的要求
    df.columns = [
        'datetime',
        'open',
        'close',
        'high',
        'low',
        'volume',
    ]
    df['openinterest'] = 0.0
    # 把 date 作为日期索引,以符合 Backtrader 的要求
    df.index = pd.to_datetime(df['datetime'])
    start_date = datetime(2022, 9, 1)  # 回测开始时间
    end_date = datetime(2023, 3, 24)  # 回测结束时间
    data = bt.feeds.PandasData(dataname=df, fromdate=start_date, todate=end_date)  # 加载数据
    cerebro.adddata(data)  # 将数据传入回测系统

    cerebro.broker.setcash(100000.0)
    # 设置佣金0.1 % ...除以100去掉 % 号。
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    cerebro.run()
    # cerebro.plot(style='candel')

    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
2023-03-23, (MA均线:  5日) 期末总资金 99201.03
2023-03-23, (MA均线: 10日) 期末总资金 101518.39
2023-03-23, (MA均线: 15日) 期末总资金 101929.14
2023-03-23, (MA均线: 20日) 期末总资金 100661.69
2023-03-23, (MA均线: 25日) 期末总资金 101611.83
2023-03-23, (MA均线: 30日) 期末总资金 101803.15

4. 总结

咱们一步一步从最原始的啥也不能做的机器人大脑,慢慢进化成能根据输入的股票数据,采用不同的策略来挣钱的成熟大脑,还能把执行过程可视化,实在是一个合格的投资者了。

后面咱们还可以:

1、自定义的指标

2、更好地管理资金;

3、详细的投资收益信息

...

所有这一切,都需要我们取挖掘,后续我们一起继续学习,掌握好Backtrader之后,就可以实现咱们自己的投资策略。

Backtrader官网

注意:

Backtrader画图matplotlib版本不要大于3.2.2,最好是用3.2.2。
python版本最好是用3.8。

我是虚拟了Backtrader环境:

conda create -n backtrader_env python=3.8
conda activate backtrader_env
pip install backtrader
pip install matplotlib==3.2.2
In [ ]: