Backtrader 策略回测初探

这篇介绍简单的回测流程,主要的内容如下:

  • 回测函数介绍
  • 单股回测
  • 多股回测

回测函数

回测策略类很简洁,直接继承 bt.Strategy ,复写父类的方法,最后把回测策略类添加到大脑即可。

回测参数 回测类参数添加通过属性变量 params 记录,可以是元组形式,也可以是字典形式。

  • 定义参数
# 元组形式,注意最后一个,逗号别删除params = (        ('maperiod', 20),    )

或# 字典形式params = {'maperiod': 20}
  • 使用
    通过 self.p.maperiod 访问提取。
bt.ind.SMA(self.data, period=self.p.maperiod)
  • 传参
    将策略类传入大脑时,传入参数
cerebro.addstrategy(TestStrategy, maperiod=5)

函数
因为继承了策略类 bt.Strategy ,运行策略时会回调这些函数,需要搞的事情是在相应的函数调用买卖逻辑即可。
先说下 lines 线对象概念,每个指标都是一条 line 线对象,贯穿所有的回测日期。bar 概念是每个日期对应所有的指标。简单理解就是 excel 表的中的列和行,line 线对象相当于 excel 的列,bar 概念相当于 excel 表的行。

函数名 函数说明 调用时机
__init__() 初始化时调用,只调用一次 初始化
next() 每条 bar 调用一次,
直到所有 bar 回测完毕,
有效的bar
每条bar都回调
prenext() 不在next()函数调用的bar,
就调用到这函数
不在next()回调的bar
notify_order() 当执行 self.buy() 、self.order_target_percent()、
self.sell()等时触发回调
订单状态发生变化回调
notify_trade() 交易改变是 交易发生时回调
log() 日志打印函数,打印一些交易信息 自己调用

还有其他一些函数,这里不一一介绍了,不是很重要的。

指标简介

指标在代码层面上的表示形式就是以 line 线对象的方式存在,贯穿整个回测周期,一般在测类类的 __init__ 函数里面构建好
在数据篇,有展示过在 feeds.Data 上直接扩展指标,下面介绍的是通过 backtrader 的指标对象来构建新的指标,新的指标也是 line 对象,贯穿整个回测周期。

下面例子构建 20 日移动平均线 SMA:

def __init__(self):    self.sma = bt.ind.SMA(self.data, period=20)

注意: 这种形式构建的指标是基于数据篇里面所说的第一个表数据的指标,并不是针对所有表的。而且会影响到 next 回调时机。20 日移动平均线的指标,前 20 日个回测 bar 是无效的,不会在 next() 函数回调,但回调到 prenext() 函数。

下图所示:

关于指标在这里不细说,后面会写一篇详细点的介绍。

回测简易配置

只是做一个简单回测测试,所以简易配置下经纪商,setcash() 配置了一小目标资产 1亿 ,设置佣金 setcommission() 千分之一,addstrategy() 添加策略并设置自定义的参数。

# 设置资产cerebro.broker.setcash(100000000.0)# 设置佣金cerebro.broker.setcommission(commission=0.001)# 添加回测策略,设置自定义参数数值cerebro.addstrategy(SingleTestStrategy, maperiod=20)# 执行策略cerebro.run()# 画图cerebro.plot()

这里不细说这配置了,后面再详细说一篇。

单股回测

下面进入主题,搞一个策略,回测下能不能赚钱,这里只是介绍使用方法,实际应用肯定不能只靠一个指标。

策略 这策略很简单,当当日收盘价高于 20 日平均线时买买买!当当日收盘价低于 20 日移动平均线时卖卖卖。

注意:

  • 该策略是以收盘价下单,以下单后的下一日开盘价作为交割价。
  • 最后一日不做任何的买卖。所以在回测的最后一日的前一天必须下单卖出股票,以便在最后一根 bar 的开盘价做为交割价卖出,不然出现未来函数。

买卖常用函数:

  1. self.order_target_percent(secu_data, target_pct, name=secu)
  2. self.order_target_value(secu_data, target_val, name=secu)
  3. self.buy(secu_data, order_amount, name=secu)
  4. self.sell(secu_data, order_amount, name=secu)

代码胜过千言万语,直接上完整代码:

import datetime

import backtrader as btimport pandas as pd

import stock_db as sdb

class SingleTestStrategy(bt.Strategy):    params = (        ('maperiod', 20),    )

    def __init__(self):        self.order = None        self.sma = bt.ind.SMA(self.data, period=self.p.maperiod)        pass

    def downcast(self, amount, lot):        return abs(amount // lot * lot)

    # 可以不要,但如果你数据未对齐,需要在这里检验    def prenext(self):        print('prenext 执行 ', self.datetime.date(), self.getdatabyname('300015')._name              , self.getdatabyname('300015').close[0])        pass

    def next(self):        # 检查是否有指令执行,如果有则不执行这bar        if self.order:            return        # 回测如果是最后一天,则不进行买卖        if pd.Timestamp(self.data.datetime.date(0)) == end_date:            return        if not self.position:  # 没有持仓            # 执行买入条件判断:收盘价格上涨突破20日均线;            # 不要在股票剔除日前一天进行买入            if self.datas[0].close > self.sma and pd.Timestamp(self.data.datetime.date(1)) < end_date:                # 永远不要满仓买入某只股票                order_value = self.broker.getvalue() * 0.98                order_amount = self.downcast(order_value / self.datas[0].close[0], 100)                self.order = self.buy(self.datas[0], order_amount, name=self.datas[0]._name)

        else:            # 执行卖出条件判断:收盘价格跌破20日均线,或者股票剔除            if self.datas[0].close < self.sma or pd.Timestamp(self.data.datetime.date(1)) >= end_date:                # 执行卖出                self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)                self.log(f'卖{self.datas[0]._name},price:{self.datas[0].close[0]:.2f},pct: 0')        pass

    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, order.Canceled, order.Margin]:            if order.isbuy():                self.log(                    f"买入{order.info['name']}, 成交量{order.executed.size},成交价{order.executed.price:.2f} 订单状态:{order.status}")                self.log('买入后当前资产:%.2f 元' % self.broker.getvalue())            elif order.issell():                self.log(                    f"卖出{order.info['name']}, 成交量{order.executed.size},成交价{order.executed.price:.2f} 订单状态:{order.status}")                self.log('卖出后当前资产:%.2f 元' % self.broker.getvalue())            self.bar_executed = len(self)

        # Write down: no pending order        self.order = None

    def log(self, txt, dt=None):        """        输出日期        :param txt:        :param dt:        :return:        """        dt = dt or self.datetime.date(0)  # 现在的日期        print('%s , %s' % (dt.isoformat(), txt))

    pass

    def notify_trade(self, trade):        '''可选,打印交易信息'''        pass

# 开始查询时间start_query = '2019-01-01'end_query = '2022-09-01'

# 开始回测时间from_date = datetime.datetime(2022, 1, 1)to_date = datetime.datetime(2022, 10, 10)cerebro = bt.Cerebro()# 添加几个股票数据codes = [    '300015',    # '300347',    # '300760',    # '603127',    # '600438']

# 添加多个股票回测数据end_date = 0for code in codes:    data = sdb.stock_daily(code, start_query, end_query)    data.index.names = ['datetime']    data_feed = bt.feeds.PandasData(dataname=data,                                    fromdate=from_date,                                    todate=to_date)    cerebro.adddata(data_feed, name=code)    end_date = data.index[-1]  # 股票剔除日    print('添加股票数据:code: %s' % code)

cerebro.broker.setcash(100000000.0)cerebro.broker.setcommission(commission=0.001)cerebro.addstrategy(SingleTestStrategy, maperiod=20)cerebro.run()cerebro.plot()

if __name__ == '__main__':    pass

结果:

em em ... 一个亿的资产,亏了接近 1000w, 策略失败!!!!

来看看回测图,backtrader 的回测图的确有点丑哈,后面会有重构可视化篇的。

最上面是资产分析图,行情数据区域的绿色三角形是买入,红色三角形是卖出。

多股回测

单股回测,上面已经介绍了,那如何多个股同时回测呢?在这个问题上,我们首先要解决的是多个股的指标计算并存储起来。

策略逻辑和上面的相同。 计算均线的时候用了dict循环计算每只股票的指标。

  1. self.getdatanames()按顺序返回所有股票的名称list
  2. self.getdatabyname(secu_name):返回该股票的data

所以,在给大脑塞数据时,需要指定 feedData 的 name ,统一用股票代码赋值,这样方便后面的索引。

直接上图说下整个流程逻辑:

代码只是再单股回测的基础下添加多股指标和多股持仓买卖判断,策略和单股相同。
代码如下:

import datetime

import backtrader as btimport pandas as pd

import stock_db as sdb

class MultiTestStrategy(bt.Strategy):    params = (        ('maperiod', 20),    )

    def prenext(self):        pass

    def downcast(self, amount, lot):        return abs(amount // lot * lot)

    def __init__(self):        # 初始化交易指令        self.order = None        self.buy_list = []        # 添加移动平均线指标,循环计算每个股票的指标        self.sma = {x: bt.ind.SMA(self.getdatabyname(x), period=self.p.maperiod) for x in self.getdatanames()}

    def next(self):        if self.order:  # 检查是否有指令等待执行            return        # 如果是最后一天,不进行买卖        if pd.Timestamp(self.datas[0].datetime.date(0)) == end_dates[self.datas[0]._name]:            return        # 是否持仓        if len(self.buy_list) < 2:  # 没有持仓            # 没有购买的票            for secu in set(self.getdatanames()) - set(self.buy_list):                data = self.getdatabyname(secu)                # 如果突破 20 日均线买买买,不要在最后一根bar的前一天买                if data.close > self.sma[secu] and pd.Timestamp(data.datetime.date(1)) < end_dates[secu]:                    # 买买买                    order_value = self.broker.getvalue() * 0.48                    order_amount = self.downcast(order_value / data.close[0], 100)                    self.order = self.buy(data, size=order_amount, name=secu)                    self.log(f"买{secu}, price:{data.close[0]:.2f}, amout: {order_amount}")                    self.buy_list.append(secu)        elif self.position:            now_lst = []            for secu in self.buy_list:                data = self.getdatabyname(secu)                # 执行卖出条件判断:收盘价格跌破20日均线,或者股票最后一根bar 的前一天之剔除日                if data.close[0] < self.sma[secu] or pd.Timestamp(data.datetime.date(1)) >= end_dates[secu]:                    # 卖卖卖                    self.order = self.order_target_percent(data, 0, name=secu)                    self.log(f"卖{secu}, price:{data.close[0]:.2f}, pct: 0")                    continue                now_lst.append(secu)            self.buy_list = now_lst

    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, order.Canceled, order.Margin]:            if order.isbuy():                self.log(f"""买入{order.info['name']}, 成交量{order.executed.size},成交价{order.executed.price:.2f}""")                self.log(                    f'资产:{self.broker.getvalue():.2f} 持仓:{[(x, self.getpositionbyname(x).size) for x in self.buy_list]}')            elif order.issell():                self.log(f"""卖出{order.info['name']}, 成交量{order.executed.size},成交价{order.executed.price:.2f}""")                self.log(                    f'资产:{self.broker.getvalue():.2f} 持仓:{[(x, self.getpositionbyname(x).size) for x in self.buy_list]}')            self.bar_executed = len(self)

        # Write down: no pending order        self.order = None

    def log(self, txt, dt=None):        """        输出日期        :param txt:        :param dt:        :return:        """        dt = dt or self.datetime.date(0)  # 现在的日期        print('%s , %s' % (dt.isoformat(), txt))

# 开始查询时间start_query = '2019-01-01'end_query = '2022-09-01'

# 开始回测时间from_date = datetime.datetime(2022, 1, 1)to_date = datetime.datetime(2022, 10, 10)cerebro = bt.Cerebro()# 添加几个股票数据codes = [    '300015',    '300347',    # '300760',    # '603127',    # '600438']

# 添加多个股票回测数据end_dates = {}end_date = 0for code in codes:    data = sdb.stock_daily(code, start_query, end_query)    data.index.names = ['datetime']    data_feed = bt.feeds.PandasData(dataname=data,                                    fromdate=from_date,                                    todate=to_date)    cerebro.adddata(data_feed, name=code)    end_dates[code] = data.index[-1]  # 股票剔除日    end_date = data.index[-1]  # 股票剔除日    print('添加股票数据:code: %s' % code)

cerebro.broker.setcash(100000000.0)cerebro.broker.setcommission(commission=0.001)cerebro.addstrategy(MultiTestStrategy, maperiod=20)cerebro.run()# 获取回测结束后的总资金portvalue = cerebro.broker.getvalue()# 打印结果print(f'结束资金: {round(portvalue, 2)}')cerebro.plot()

if __name__ == '__main__':    pass

看下日志:

enen ,居然赚钱了,小赚接近 1000w.

看下 backtracder 的买卖点:

后面日期好像没触发买卖逻辑,这后面再看看是咋回事。

写于 2022 年 10 月 23 日 10:27:29

本文由 mdnice 多平台发布

Backtrader 策略回测初探相关推荐

  1. python 策略回测期货_量化投资实战教程(1)—基于backtrader的简单买入卖出策略

    都说Python可以用于量化投资,但是很多人都不知道该怎么做,甚至觉得是非常高深的知识,其实并非如此,任何人都可以在只有一点Python的基础上回测一个简单的策略. Backtrader是一个基于Py ...

  2. backtrader程序介绍-策略回测用法

    backtrader的策略回测初尝 前言 backtrader作为能够在自己的python环境运行的回测程序之一,不得不说很好用.今天进行了初步的学习,稍微进行分享. 一.回测基础步骤 应用backt ...

  3. 【手把手教你】Ichimoku云图指标可视化与交易策略回测

    01 引言 Ichimoku Kinko Hyo,简称Ichimoku,是一名日本报纸作家提出的,用于衡量动量以及未来价格支撑和阻力区域的技术分析指标,目前被广泛用于判断外汇.期货.股票.黄金等投资品 ...

  4. Python双均线策略回测(2021-10-12)

    Python双均线策略回测 1.择时策略简介 根据百度百科的解释,择时交易是指利用某种方法来判断大势的走势情况,是上涨还是下跌或者是盘整.如果判断是上涨,则买入持有:如果判断是下跌,则卖出清仓,如果是 ...

  5. Excel-VBA 股票网格交易策略回测

    大家好,我是陈小虾,是一名自动化方向的IT民工.写博客是为了记录自己的学习过程,通过不断输出倒逼自己加速成长.功能说明:由于水平有限,博客中难免会出现一些BUG,或者有更优方案恳请各位大佬不吝赐教!微 ...

  6. 可转债网格交易策略回测

    什么是网格交易策略:基于股票波动高抛低吸策略,自动化反复买卖赚取差价.投资者借助条件单,把资金分成多份,从基准价开始,每跌x%就自动买入一份,每涨y%就自动卖掉一份.股价越波动高抛低吸的机会越多 什么 ...

  7. 用趋势突破策略回测CTA

    以下是小哥用趋势突破策略回测CTA的代码和结果,错误的地方还请大家提出来. 标的是螺纹钢的主力连续合约 #%% 趋势突破策略 # 导入包 import pandas as pd import matp ...

  8. python 股票回测书籍推荐_python实现马丁策略回测3000只股票

    python实现马丁策略回测3000只股票 批量爬取股票数据 这里爬取数据继续使用tushare,根据股票代码来遍历,因为爬取数据需要一定时间,不妨使用多线程来爬取,这里要注意tushare规定每分钟 ...

  9. python 量化策略回测_在python中创建和回测对交易策略

    python 量化策略回测 Pairs trading is one of the many mean-reversion strategies. It is considered non-direc ...

最新文章

  1. AC日记——小书童——刷题大军 洛谷 P1926
  2. 最全总结!聊聊 Python 操作PDF的几种方法
  3. java.lang.NoSuchMethodException: tk.mybatis.mapper.provider.base.BaseSelectProvider.<init>()的问题解决
  4. 【已解决】surefire-reports for the individual test results.
  5. mysql_install_db卸载_MySQL数据库的卸载与安装
  6. 在Qt Creator以外编写Qt程序
  7. 《Spark与Hadoop大数据分析》一一1.1 大数据分析以及 Hadoop 和 Spark 在其中承担的角色...
  8. Qt5 QtQuick系列----QtQuick的Secne Graph剖析(1)
  9. FastDFS入门一篇就够
  10. Atitit import sql fun 重要的sql功能扩展 ext 目录 1.1. Insert merge 1 1.2. Insert set 1 1.2.1. 13.2.5. LOAD
  11. 设置Windows系统双网卡同时上内外网
  12. [FOI2020WC模拟]看上去很简单
  13. bugly怎么读_Bugly迁入
  14. 浙江大学【面板数据分析与STATA应用】——第四讲动态面板数据类型
  15. 学大数据一定要会Java开发吗?
  16. 计算机发展简史的ppt教程,计算机发展简史PPT.ppt
  17. 普通人有没有必要学python,什么样的人适合学python
  18. L1-051 打折 (5 分)
  19. Arduino+NRF24L01制作遥控器
  20. Ntrip协议访问千寻位置服务

热门文章

  1. 智能汽车“三国杀”,哪种模式即将突围?
  2. TISS与HashQuark达成战略合作 | TokenInsight
  3. 大班防暴安全安全教案
  4. 【GlobalMapper精品教程】048:空间操作(5)——对称差集(Symmetrical Difference)
  5. 传统目标跟踪——光流法
  6. 计算机专业创业实践课论文,计算机专业创业论文.doc
  7. 因为计算机中丢失dui,修复WmpDui.dll
  8. citra linux安装教程,Citra3ds模拟器配置需求说明
  9. golang 运行命令行进行图片格式转换(jpg转png)
  10. Python绘制股票日K图(十)汇总日K图、柱状图、折线图