前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Backtrader来啦:常见问题汇总

Backtrader来啦:常见问题汇总

作者头像
量化投资与机器学习微信公众号
发布2023-12-20 19:10:48
6280
发布2023-12-20 19:10:48
举报

量化投资与机器学习微信公众号,是业内垂直于量化投资、对冲基金、Fintech、人工智能、大数据等领域的主流自媒体。公众号拥有来自公募、私募、券商、期货、银行、保险、高校等行业30W+关注者,曾荣获AMMA优秀品牌力、优秀洞察力大奖,连续4年被腾讯云+社区评选为“年度最佳作者”。

量化投资与机器学习公众号 独家撰写

量化投资与机器学习公众号为全网读者带来的Backtrader系列,自推出以来收获无数好评!我们是真的在用心做这个内容。

QIML针对这个系列的宗旨就是:

免费!

做最好、最清晰的Bt教程!

让那些割韭菜的课程都随风而去吧!

为此,QIML为大家多维度、多策略、多场景来讲解Backtrader:

  • Backtrader 常见问题汇总(今日)

同时,我们对每段代码都做了解读说明,愿你在Quant的道路上学有所获!

希望大家多Follow,多给星 ★

常见问题

1、如何直接从Mysql数据库中加载数据?

Backtrader的DataFeeds数据模块提供了各种加载数据的方法,之前的文章有介绍如何加载CSV文件或DataFrame中的数据,今天就补充介绍如何直接从Mysql数据库中加载数据。

下面的例子就是在继承了DataBase父类的基础上,修改相关方法的操作逻辑,“改装”得到了一个新的DataFeeds类,类名为 PsqlDatabase:

代码语言:javascript
复制
import datetime as dt
import backtrader as bt
from backtrader import DataBase, date2num

class PsqlDatabase(DataBase):
    '''
    默认数据库表格字段如下:
            ticker char(5),
            date date,
            high numeric(10,4),
            low numeric(10,4),
            open numeric(10,4),
            close numeric(10,4),
            volume integer,
            unique (ticker, date)
    '''
    params = (
        # 数据库连接信息
        ('user', None),
        ('password', None),
        ('host', None),
        ('port', None),
        ('dbname', None),
        ('table', None),
        # 证券信息
        ('ticker', None), # 要提取的证券代码
        ('fromdate', None), # 提取数据的起始时间(包含)
        ('todate', None), # 提取数据的截止时间(包含)
        # 每条线对应的提取出来的数据的列索引
        ('datetime', 0),
        ('high', 1),
        ('low', 2),
        ('open', 3),
        ('close', 4),
        ('volume', 5),
        ('openinterest', -1), # -1 表示不存在该列数据
    )

    def start(self):
        conn = self._connect_db()
        query = ("""SELECT date, high, low, open, close, volume """
                 """FROM {table} """
                 """WHERE ticker = '{ticker}' """
                 .format(table=self.p.table,
                         ticker=self.p.ticker))
        if self.p.fromdate is not None:
            query += " AND date >= '{fromdate}' ".format(fromdate=dt.datetime.strftime(self.p.fromdate, '%Y-%m-%d'))
        if self.p.todate is not None:
            query += " AND date <= '{todate}' ".format(todate=dt.datetime.strftime(self.p.fromdate, '%Y-%m-%d'))
        query += """ORDER BY date asc"""

        self.result = conn.execute(query)
        self.price_rows = self.result.fetchall()
        self.result.close()
        self.price_i = 0
        super(PsqlDatabase, self).start()

    def _load(self):
        if self.price_i >= len(self.price_rows):
            return False
        # 每循环一次_load(),填充一个 bar 的数据
        row = self.price_rows[self.price_i]
        self.price_i += 1
        for datafield in self.getlinealiases(): # 查看 Data Feeds 包含哪些线
            if datafield == 'datetime':
                self.lines.datetime[0] = date2num(row[self.p.datetime])
            elif datafield == 'volume':
                self.lines.volume[0] = row[self.p.volume]
            else:
                colidx = getattr(self.params, datafield) # 获取列索引
                if colidx < 0: # 列索引小于0,表示不存在该列
                    continue
                line = getattr(self.lines, datafield) # 将数据赋值给对应的线
                line[0] = float(row[colidx])
        return True
    
    # 设置数据库连接逻辑
    def _connect_db(self):
        from sqlalchemy import create_engine
        url = 'mysql+mysqldb://{user}:{password}@{host}:{port}/{dbname}'.format(user=self.p.user,
                                                                         password=self.p.password,
                                                                         host=self.p.host,
                                                                         port = self.p.port,
                                                                         dbname=self.p.dbname)
        engine = create_engine(url, echo=False)
        conn = engine.connect()
        return conn

    def preload(self):
        # 负责循环调用load()(_load()是被 load() 调用的)
        super(PsqlDatabase, self).preload()
        # self.price_rows 的数据都存入lines后,清除 self.price_rows 中的数据,释放资源
        self.price_rows = None
        
cerebro = bt.Cerebro()
# 调用 MysqlData 类,得到实例
data = PsqlDatabase(user='xxxxx',
                    password='xxxx',
                    host='xxx',
                    port='xxxx',
                    dbname='xxxx',
                    table='xxxx',
                    ticker='xxxxx',
                    fromdate='xxxxx',
                    todate='xxxxx')
cerebro.adddata(data, name='xxxx') # 将数据传给大脑
  • params 属性对应的是加载数据时涉及的各种参数,主要是新增了一部分和数据库有关的信息,7 条基础 lines 的索引需要与 sql 语句中字段的顺序相一致;
  • start() 方法用于启动数据加载,连接数据库、从数据库中读取数据等操作逻辑会写在该方法中;
  • stop() 方法用于关闭数据加载,断开数据库连接的操作逻辑可以写在该方法中(上例未涉及stop());
  • _load() 方法负责将加载的数据,一个个赋值给 7 条基础 lines,直到所有数据都已填充进 lines 为止(返回 False);
  • preload() 方法负责不断的循环调用 load()(_load()是被 load() 调用的)直到下载完所有数据;
  • 上面这些方法都是底层 DataBase 类中的方法,想要具体了解可以看底层代码 backtrader/feed.py at master · mementum/backtrader (github.com);
  • 上面这个案例参考的 Github 中的 PSQL feed implementation by dolanwill · Pull Request #393 · mementum/backtrader (github.com),以及 Backtrader 社区中的讨论 SQLite example | Backtrader Community;
  • Backtrader 的 DataFeeds 数据模块提供的 InfluxDB 类也是类似的实现逻辑:backtrader/influxfeed.py at master · mementum/backtrader (github.com);
  • 如果想连接不同的数据库,只需修改数据库连接方法 _connect_db()、start() 中的查询语句等逻辑即可。

2、出现 AttributeError: 'int' object has no attribute 'to_pydatetime' 报错?

大家在用PandasData往大脑cerebro中adddata基础行情数据时,如果遇到AttributeError: 'int' object has no attribute 'to_pydatetime' 报错,是因为:没有将 datetime 设置为 index, 或者是没有指定 datetime 所在的列。

代码语言:javascript
复制
...
    params = (
        # Possible values for datetime (must always be present)
        # None : datetime is the "index" in the Pandas Dataframe
        # -1 : autodetect position or case-wise equal name
        # >= 0 : numeric index to the colum in the pandas dataframe
        # string : column name (as index) in the pandas dataframe
        ('datetime', None),
...

# PandasData 默认是将 DataFrame 的索引作为 datetime
# 如果你已经将 datetime 设置为 index ,可以直接用下面的语句导入数据:
data = bt.feeds.PandasData(dataname=price)
# 如果 datetime 只是 DataFrame 中的一列,且列名称也一致(不区分大小写),则需要设置参数:
data = bt.feeds.PandasData(dataname=price, datetime=-1)
# 或是指定 datetime 在第几列,比如在 DataFrame 的第 7 列,则令 datetime=6
data = bt.feeds.PandasData(dataname=price, datetime=6)

3、出现create_full_tear_sheet() got an unexpected keyword argument 'gross_lev' 报错?

在回测完成后,我们可以借助Backtrader的策略分析器模块analyzer返回诸多的策略收益评价指标,而且Backtrader还集成了Quantoption的Pyfolio模块。Backtrader中的PyFolio分析器是由TimeReturn、PositionsValue、Transactions、GrossLeverage4个子分析器构成的,PyFolio分析器会一次性返回上述4个自分析器的计算结果,分析结果的可视化展示还是通过调用Quantoption的Pyfolio模块来实现:

代码语言:javascript
复制
...
# 添加 PyFolio 分析器
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
...
results = cerebro.run()
strat = results[0]
# 一次性获取 4 个子分析器的计算结果
pyfoliozer = strat.analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
...
...
# 利用 Quantoption 的 Pyfolio 模块来绘制图形
# 需要提前安装好该模块 pip install pyfolio==0.5.1
import pyfolio as pf
pf.create_full_tear_sheet(
    returns,
    positions=positions,
    transactions=transactions,
    gross_lev=gross_lev,
    live_start_date='2005-05-01', # This date is sample specific
    round_trips=True)

如果出现 create_full_tear_sheet() got an unexpected keyword argument 'gross_lev' 报错,是因为后期版本更新后的 create_full_tear_sheet 不再支持 gross_lev 这个参数,官方文档给出的解释如下:

代码语言:javascript
复制
As of (at least) 2017-07-25 the pyfolio APIs have changed and create_full_tear_sheet no longer has a gross_lev as a named argument.

所以在使用 create_full_tear_sheet 事,不要设置 gross_lev 参数,以及令 round_trips 为 False:

代码语言:javascript
复制
import pyfolio as pf
fig = pf.create_full_tear_sheet(
            returns,
            positions=positions,
            transactions=transactions,
            # gross_lev=gross_lev, 
            live_start_date='2020-05-01',
            round_trips=False,
            return_fig = True # 后期用于存储
            )

# fig.savefig('returns_tear_sheet.pdf')

如果遇到新的报错:AttributeError: ‘numpy.int64’ object has no attribute ‘to_pydatetime’,建议卸载 pyfolio 重新从 git 上拉代码安装:

代码语言:javascript
复制
pip uninstall pyfolio
pip install git+https://github.com/quantopian/pyfolio

4、如何添加业绩基准Benchmark?

Backtrader中与业绩基准相关的操作主要有 2 种方式:

  • 一种是通过 bt.analyzers.TimeReturn 返回业绩基准的收益率,在此之前,需要确保已经将业绩基准的行情数据adddata给大脑,还要给 bt.analyzers.TimeReturn 指定 data 参数;
  • 另一种是通过 bt.observers.Benchmark 添加业绩基准的观测器,plot绘图时展示的收益率曲线就是 bt.analyzers.TimeReturn 返回的收益率。
代码语言:javascript
复制
# 实例化大脑
cerebro = bt.Cerebro() 
# 初始资金 1,000,000 
cerebro.broker.setcash(1000000.0) 
# 读取行情数据 
daily_price = pd.read_csv("./data/daily_price.csv", parse_dates=['datetime'])
stock_price = daily_price.query(f"sec_code=='600718.SH'").set_index('datetime')
datafeed1 = bt.feeds.PandasData(dataname=stock_price,
                                fromdate=pd.to_datetime('2019-01-02'), 
                                todate=pd.to_datetime('2021-01-28')) 
cerebro.adddata(datafeed1, name='600718.SH') 
benchmark_price = daily_price.query(f"sec_code=='600728.SH'").set_index('datetime')
datafeed2 = bt.feeds.PandasData(dataname=benchmark_price,
                                fromdate=pd.to_datetime('2019-01-02'), 
                                todate=pd.to_datetime('2021-01-28'),
                                ) 
cerebro.adddata(datafeed2, name='600728.SH')

# 将编写的策略添加给大脑,别忘了 !
cerebro.addstrategy(TestStrategy)
cerebro.addanalyzer(bt.analyzers.TimeReturn,_name='stock_returns')
# 返回 benchmark 的收益率
cerebro.addanalyzer(bt.analyzers.TimeReturn, data=datafeed2, _name='benchmark_returns')
# 添加业绩基准的观测器
cerebro.addobserver(bt.observers.Benchmark, data=datafeed2)
cerebro.addobserver(bt.observers.TimeReturn)
result = cerebro.run() 
cerebro.plot(iplot=True)

相关参考:https://www.backtrader.com/blog/posts/2016-07-22-benchmarking/benchmarking/

5、如何设置非整数型的成交数量?

Backtrader在撮合成交订单时,订单上的购买数量都是算的整数,但是像比特币这类加密货币的交易是会出现小数的成交数量的,比如交易 0.5 个比特币,那如何设置非整型的成交数量呢?只需通过继承 bt.CommissionInfo 重新定义获取成交量 getsize 即可:

代码语言:javascript
复制
class CommInfoFractional(bt.CommissionInfo):
    def getsize(self, price, cash):
        '''Returns fractional size for cash operation @price'''
        return self.p.leverage * (cash / price)
        
# 然后通过 addcommissioninfo 将设置传递给 broker
cerebro.broker.addcommissioninfo(CommInfoFractional())

默认情况下的 getsize 的定义如下所示,其实只需将取整相关的逻辑(int、整除)删除即可:

代码语言:javascript
复制
# 默认情况下的 getsize 的定义如下,只需
def getsize(self, price, cash):
          '''Returns the needed size to meet a cash operation at a given price'''
          if not self._stocklike:
              return int(self.p.leverage * (cash // self.get_margin(price)))
  
          return int(self.p.leverage * (cash // price)

相关参考:https://www.backtrader.com/blog/posts/2019-08-29-fractional-sizes/fractional-sizes/

6、Backtrader 如何处理股票拆分合并、分红配股的情况?

当股票发生拆分合并或是分红配股时,股票价格会发生较大的变动,使得当前价格变得不连续而出现断层现象,为了保持价格的连续性,都会对价格做复权处理。

回测时遇到上述情况,最符合现实的操作是:交易时仍用真实价格(不复权)作为委托价进行下单,计算交易数量;但在计算涨跌或收益时,会考虑股价的连续性(使用复权后的价格),防止价格断层扭曲真实收益。

目前Backtrader还无法处理股票拆分合并、分红配股带来的影响,但常规的处理方式是在导入行情数据时,就直接导入复权后的行情数据(一般选择后复权),保证收益的准确性。

结语

至此,本次Backtrader系列已全部更新完毕。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 量化投资与机器学习 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 量化投资与机器学习微信公众号,是业内垂直于量化投资、对冲基金、Fintech、人工智能、大数据等领域的主流自媒体。公众号拥有来自公募、私募、券商、期货、银行、保险、高校等行业30W+关注者,曾荣获AMMA优秀品牌力、优秀洞察力大奖,连续4年被腾讯云+社区评选为“年度最佳作者”。
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档