量化投资与机器学习微信公众号,是业内垂直于量化投资、对冲基金、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 返回的收益率。
# 实例化大脑
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系列已全部更新完毕。