基于Python的量化交易回测框架Backtrader初识记录(一)

版权声明:本文为博主原创文章,如需转载请贴上原博文链接:基于Python的量化交易回测框架Backtrader初识记录(一)-CSDN博客


前言:近期以来,对股市数据获取及预处理算是告一段落,下一步就需要利用这些实盘数据进行数据分析并制定策略。本文目的旨在记录在操作Backtrader(正文内容中简称BT)的过程中的思路以及面临新问题的解决方法,并非指导教程,入门教程见第一章《Quick Start》。

        在选择Backtrade之前,有查找过目前市场上主流的一些量化交易平台及框架,量化平台比如:米筐、聚宽以及同花顺旗下的SuperMind等,框架比较有名的有:vnpy以及AKfamily中的PyBroker(AKShare就是出自他们家),而首选Backtrader的原因有这几点:

①在本地建立了数据库的情况下能离线运行;

②有详尽的文档说明;

③代码开源且覆盖py2和py3(py2不支持图像展示);

④集成ta-lib金融市场技术分析库;

⑤框架简便清晰,能为日后使用AI进行量化策略研究降低入门门槛;

        当然Backtrader也不是没有缺点,比如适用于中低频,高频量化就不太行;而且看到有别的使用者说,数据量过大或者订单数过多会导致回测时间太长,对内存需求太大;还有目前官方GitHub已经一年多未更新了,不知道后续是否能持续维护。


目录

一、Quick Start

二、Backtrader中的数据形式探讨

2.1 BT数据源

2.2 BT中两种数据形式对比

2.2.1 Yahoo数据形式

2.2.2 Pandas数据形式

 2.2.3 GenericCSVData数据形式

三、实例“000001.SZ 平安银行”的简单量化策略回测

3.1 通过从数据库中获取数据来实现策略回测

3.2 通过从CSV文件中获取数据来实现策略回测

3.3 两种数据获取方式结果对比

参考资料


一、Quick Start

        对于快速上手,可参考:从零开始掌握BackTrader量化框架,这一专栏中的五篇文章能迅速帮助建立框架认知,并进行简单的策略操作,同时能展示出股票走势、买卖点指示以及收益情况。

二、Backtrader中的数据形式探讨

        目前网络上大多数给出的BT输入的用于回测的数据都是Yahoo的CSV文件(在线或下载下来的csv文件),显然这种方式只适用于针对策略研究,而不适用于选股以及量化操作,毕竟一个一个单独的csv文件加载起来可不是一件高效的事情,更何况Yahoo自2021年11月1日起已经不再给中国大陆提供产品与服务了。

2.1 BT数据源

        从官方手册可见,支持多种数据源输入(如图2.1所示),然而并没有找到我想使用的方式,但是看到支持Pandas,那就可以从数据库读取数据了。

图2.1 Data Feeds

2.2 BT中两种数据形式对比

2.2.1 Yahoo数据形式

        在Yahoo还能使用的时候,通常可以直接从Yahoo网站上拉取数据,参考:股票OHLC历史数据爬取——Yahoo,查看源码可发现,在yahoo.py文件中的YahooFinanceCSVData类,使用方法如下:

# `filepath`为csv文件的路径,sd为start date,ed为end date
filepath = '***/***.csv'
sd, ed = datetime(2018, 1, 1), datetime(2019, 12, 31)
data = bt.feeds.YahooFinanceCSVData(dataname=filepath, fromdate=sd, todate=ed)

需要的数据如下:

# path:./backtrader/feeds/yahoo.py
self.lines.datetime[0] = dtnum
self.lines.openinterest[0] = 0.0
self.lines.open[0] = o
self.lines.high[0] = h
self.lines.low[0] = l
self.lines.close[0] = c
self.lines.volume[0] = v
self.lines.adjclose[0] = adjustedclose

其中主要参数读取顺序(自上而下)如下图2.2: 

图2.2 读取的csv文件中必须包含的7个参数

        当然,如果数据库中没有adjclose数据(一般前复权/后复权数据需要足够的权限),也可以直接使用`close`的数据替代(当然这样有时候会有些许不准确,或者在构建数据库的时候,close数据直接使用前复权的收盘价);

        如图2.3,数据加载之后的顺序有些混乱,暂时还没搞清楚是什么原因导致的,后续再研究。(注:line_7是adjustedclose)【原因已清楚,见2.2.3节对应位置】;

图2.3 line中的参数顺序_Yahoo

2.2.2 Pandas数据形式

        通常处理Dataframe类型的数据就使用Pandas,BT中有和Pandas相关的数据类型,查看源码可发现,在pandafeed.py文件中的PandasData类,使用方法如下:

# df为从数据库中读取的DataFrame类型的个股数据,sd和ed分别为起始和终了时间
sd, ed = datetime(2018, 1, 1), datetime(2019, 12, 31)
data = bt.feeds.PandasData(dataname=df, fromdate=sd, todate=ed)

需要的数据如下:

# path:./backtrader/feeds/pandafeed.py
params = (
    ('nocase', True),
    ('datetime', None),
    ('open', -1),
    ('high', -1),
    ('low', -1),
    ('close', -1),
    ('volume', -1),
    ('openinterest', -1),
)

# 主要参数读取顺序
datafields = [
    'datetime', 'open', 'high', 'low', 'close', 'volume', 'openinterest'
]

        目前数据库中个股数据来源于Tushare,其column name(如图2.4所示)和PandaData所需的datafields并未对齐,所以从数据库加载的时候可以将多余的字段删除(当然openinterest本身就没有这个字段的数据,也不影响策略回测计算和后续绘图);

图2.4 MySQL数据库中保存的个股数据的字段名称

        采用PandaData方式加载的数据形式如图2.5所示,可见比使用YahooFinanceCSVData方式加载的数据少了一个`adjclose`字段,但如同2.2.1节中所说的,可以直接将前复权/后复权的收盘价存储在`close`字段中;(注:line_6是datetime);

图2.5 line中的参数顺序_Pandas

 2.2.3 GenericCSVData数据形式

        在无法使用Yahoo获取数据后,依然能使用BT提供的`Generic CSV support`方式来读取csv文件,查看源码可发现,在csvgeneric.py文件中的GenericCSVData类,使用方法如下:

# `filepath`为csv文件的路径,sd为start date,ed为end date
filepath = '***/***.csv'
sd, ed = datetime(2018, 1, 1), datetime(2019, 12, 31)

# dtformat='%Y%m%d'表示csv文件中的datetime列的日期参数数据形式是'YYYYMMDD'
# volume=8表示在csv文件中,volume字段在第8列(datetime是第0列)
data = bt.feeds.GenericCSVData(dataname=filepath, fromdate=sd, todate=ed, dtformat='%Y%m%d', volume=8, openinterest=-1) 

 需要的数据如下:

# ./backtrader/feeds/csvgeneric.py
params = (
    ('nullvalue', float('NaN')),
    ('dtformat', '%Y-%m-%d %H:%M:%S'),
    ('tmformat', '%H:%M:%S'),

    ('datetime', 0),
    ('time', -1),
    ('open', 1),
    ('high', 2),
    ('low', 3),
    ('close', 4),
    ('volume', 5),
    ('openinterest', 6),
)

         csv文件中的参数字段如图2.6所示,其中需要注意的是,程序读取该csv文件时,“trade_date”默认是第0列,而在读取成交量“vol”数据时,要设置`volume=8`,因为在"vol"之前还有其他三个参数,是用不到的;

图2.6 csv中个股数据的字段

        采用GenericCSVData方式加载的数据形式如图2.7所示,2.2.1节中所说的参数存储顺序混乱其实是因为参数保存是按照内存地址顺序进行存储的,故内存地址在前的参数字段就靠前(详情还需查看官方文件,看Line是如何对数据进行读取的),如图2.8所示;

图2.7 line中的参数顺序_GenericCSVData
图2.8 各个参数储存的内存地址

三、实例“000001.SZ 平安银行”的简单量化策略回测

        实例完整代码见:A股行情数据获取&量化交易策略的结构设计,考虑到Yahoo的csv数据格式固定(若需复现得要修改yahoo.py源码),且Yahoo在中国大陆已不再支持,故本章只复现2.2.2和2.2.3节中的内容;

3.1 通过从数据库中获取数据来实现策略回测

        该方式对于尚未建立数据库的学习者来说较复杂,建议直接使用3.2节中的方法(下载两个文件就能复现);若已经建立了数据库并实现了所有个股数据存储到数据库后,直接运行Strategy文件夹下的“demo_strategy_db.py”文件即可,以下为该文件完整代码:

"""
Created on 2024年09月02日

@author: Aiden_yang
@website:https://gitee.com/aiden_yang/Stocks/tree/dev/Strategy/demo_strategy_db.py
"""
from datetime import datetime

import pandas as pd
import backtrader as bt

from Module import MySQL_Database, mysql_data_processing as mdp
from config import stock_table_name
from Strategy import cash


class MyStrategy(bt.Strategy):
    """"""
    params = (('period', 10),)  # 设置周期为10天

    def __init__(self, ):
        """Constructor for MyStrategy"""
        # 引用到输入数据的close价格
        # 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  # To keep track of pending orders
        self.buyprice = None
        self.buycomm = None

        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.MovingAverageSimple()
        # self.sma = bt.indicators.MovingAverageSimple(period=self.params.period)   # 默认均线周期是30天,可自行设置

    def log(self, txt, dt=None):
        """
        # 提供记录功能 Logging function for this strategy
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify_order(self, order):
        """
        order.Status:[0~8]
        ['Created', 'Submitted', 'Accepted', 'Partial', 'Completed', 'Canceled', 'Expired', 'Margin', 'Rejected']
        :param order:
        :return:
        """
        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
        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
            elif order.issell():
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm: %.2f' %
                         (order.executed.price, order.executed.value, order.executed.comm))
                pass

        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 notify_trade(self, trade):  # 交易执行后,在这里处理
        if not trade.isclosed:
            return

        # 记录下盈利数据(GROSS:毛利,NET:净利)
        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm))

    def next(self):
        """
        # 策略核心:均线买卖策略——收盘价大于均线则买入,收盘价低于均线则卖出
        :return:
        """
        # 目前的策略就是简单显示下收盘价,Simple 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]:  # 大于均线买入
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.order = self.buy()  # Keep track of the created order to avoid a 2nd order
        else:
            if self.dataclose[0] < self.sma[0]:  # 小于均线卖出
                self.log('SELL CREATED, %.2f' % self.dataclose[0])
                self.order = self.sell()  # Keep track of the created order to avoid a 2nd order


def get_database_data(stn):
    """
    # 加载数据库中的个股数据
    :param stn:
    :return:
    """
    # 从数据库中获取所需回测的股票数据名称
    database = MySQL_Database.MySQLDatabaseOperations()
    all_stock_basic = database.read_data(stn)  # 获取数据表“all_stock_basic”中所有上市股票的代码
    stock_name = mdp.tscode_to_tbname(all_stock_basic.ts_code[0])  # 取数据库中第一只股票的代码名字

    # 加载该个股OHLC及成交量等数据,并使用交易日期作为DataFrame的index
    df = database.read_data(stock_name)
    df.index = pd.to_datetime(df['trade_date'])

    # 重组df的列名称,让其符合backtrader要求的格式
    df.drop(['ts_code', 'trade_date', 'pre_close', 'change', 'pct_chg', 'amount'], axis=1, inplace=True)
    df.rename(columns={'vol': 'volume'}, inplace=True)
    return df


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

    # 设置一个回测策略
    cerebro.addstrategy(MyStrategy)

    # 获取数据库中的个股数据
    stock_df = get_database_data(stock_table_name)

    # 将数据适配到bt框架的数据类型
    start_date = datetime(2024, 1, 1)  # 回测开始时间
    end_date = datetime.now()  # 回测结束时间
    data = bt.feeds.PandasData(dataname=stock_df, fromdate=start_date, todate=end_date)

    # 将数据传入回测系统
    cerebro.adddata(data)

    # 设定初始金额
    cerebro.broker.setcash(cash=cash)

    # 设置佣金:千分之一
    cerebro.broker.setcommission(commission=0.001)

    # 设置每次交易买入的股数
    cerebro.addsizer(bt.sizers.FixedSize, stake=100)

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

    cerebro.plot(style='candle')

         运行结果如图3.1所示,K线展示如图3.2所示:

图3.1 db_print
图3.2 db_kline

3.2 通过从CSV文件中获取数据来实现策略回测

        想要快速上手复现出回测结果,从量化交易策略模块中下载“demo_strategy_csv.py”和“sz000001.csv”文件(如图3.3所示),完整代码如下:

图3.3 策略模块
"""
Created on 2024年09月02日

@author: Aiden_yang
@website:https://gitee.com/aiden_yang/Stocks/tree/dev/Strategy
"""
import os
from datetime import datetime

import backtrader as bt


class MyStrategy(bt.Strategy):
    """"""
    params = (('period', 10),)  # 设置周期为10天

    def __init__(self, ):
        """Constructor for MyStrategy"""
        # 引用到输入数据的close价格
        # 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  # To keep track of pending orders
        self.buyprice = None
        self.buycomm = None

        # Add a MovingAverageSimple indicator
        self.sma = bt.indicators.MovingAverageSimple()
        # self.sma = bt.indicators.MovingAverageSimple(period=self.params.period)   # 默认均线周期是30天,可自行设置

    def log(self, txt, dt=None):
        """
        # 提供记录功能 Logging function for this strategy
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify_order(self, order):
        """
        order.Status:[0~8]
        ['Created', 'Submitted', 'Accepted', 'Partial', 'Completed', 'Canceled', 'Expired', 'Margin', 'Rejected']
        :param order:
        :return:
        """
        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
        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
            elif order.issell():
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm: %.2f' %
                         (order.executed.price, order.executed.value, order.executed.comm))
                pass

        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 notify_trade(self, trade):  # 交易执行后,在这里处理
        if not trade.isclosed:
            return

        # 记录下盈利数据(GROSS:毛利,NET:净利)
        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm))

    def next(self):
        """
        # 策略核心:均线买卖策略——收盘价大于均线则买入,收盘价低于均线则卖出
        :return:
        """
        # 目前的策略就是简单显示下收盘价,Simple 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]:  # 大于均线买入
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                self.order = self.buy()  # Keep track of the created order to avoid a 2nd order
        else:
            if self.dataclose[0] < self.sma[0]:  # 小于均线卖出
                self.log('SELL CREATED, %.2f' % self.dataclose[0])
                self.order = self.sell()  # Keep track of the created order to avoid a 2nd order


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

    # 设置一个回测策略
    cerebro.addstrategy(MyStrategy)

    # 获取数据库中的个股数据
    filepath = os.path.abspath(os.path.dirname(__file__))
    filename = 'sz000001.csv'
    csvpath = filepath + '/' + filename

    # 将数据适配到bt框架的数据类型
    start_date = datetime(2024, 1, 1)  # 回测开始时间
    end_date = datetime.now()  # 回测结束时间
    data = bt.feeds.GenericCSVData(dataname=csvpath, fromdate=start_date, todate=end_date, dtformat='%Y%m%d', volume=8,
                                   openinterest=-1)  # volume=8表示在csv文件中,volume字段在第8列(datetime是第0列)

    # 将数据传入回测系统
    cerebro.adddata(data)

    # 设定初始金额
    cash = 100000.0
    cerebro.broker.setcash(cash=cash)

    # 设置佣金:千分之一
    cerebro.broker.setcommission(commission=0.001)

    # 设置每次交易买入的股数
    cerebro.addsizer(bt.sizers.FixedSize, stake=100)

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

    cerebro.plot(style='candle')

        运行结果如图3.4所示,K线展示如图3.5所示:

图3.4 csv_print
图3.5 csv_kline

3.3 两种数据获取方式结果对比

        从两种输出(图3.1和3.4)、K线展示及买卖点标注(图3.2和3.5),结果完全一样;至此,简单的策略回测已经完成了,至于能否根据策略盈利,就看个人设计的量化策略的能力了。


参考资料

1.Backtrader简介

2.【答读者问3】用backtrader可以做什么?

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值