backtrader是基于Python的量化回测框架,优点是运行速度快,支持pandas的矢量运算;支持参数自动寻优运算,内置了talib股票分析技术指标库;支持多品种、多策略、多周期的回测和交易;支持pyfolio、empyrical分析模块库、alphalens多因子分析模块库等;扩展灵活,可以集成TensorFlow、PyTorch和Keras等机器学习、神经网络分析模块。
(扫描本文最下方二维码获取全部完整源码和Jupyter Notebook 文件打包下载。)
我们将首先总结 backtrader 的关键概念,以阐明在此平台上的回测工作流程,然后演示回测的使用方法,并通过机器学习进行预测。
Cerebro 架构的关键概念
backtrader 的 Cerebro(西班牙语为“大脑”)架构将回测工作流的关键组件表示为(可扩展的)Python 对象。这些对象相互作用以促进处理输入数据和因子计算、制定和执行策略、接收和执行订单以及跟踪和测量性能。Cerebro 调度从收集输入、逐条执行回测和提供结果的整个过程。
下图概述了 Cerebro 架构中的关键元素:
Data feeds
、lines
和 indicators
Data feeds
是策略的原材料,包含有关单个证券的信息,例如每次观察都带有时间戳的 OHLCV 市场数据,但您可以自定义可用字段。backtrader 可以从各种来源(包括 CSV 文件和 Pandas DataFrames)以及雅虎财经等在线来源获取数据。您还可以使用扩展程序连接到盈透证券等在线交易平台,以获取实时数据并执行交易。与 DataFrame 对象的兼容性意味着您可以从可访问的 Pandas 加载数据,范围从数据库到 HDF5 文件。
加载数据后,我们将
data feeds
添加到 Cerebro 实例,然后按照收到的顺序使其可用于一个或多个策略。您的策略的交易逻辑可以按名称(例如,股票代码)或序列号访问每个
data feed
,并检索
data feed
任何字段的当前和过去值。每个字段称为一行。
backtrader 带有 130 多个常用技术指标,让您可以根据每个
data feed
的
line
或其他指标计算新值,以运行您的策略。您还可以使用标准 Python 操作来派生新值。用法相当简单,并且在文档中得到了很好的解释。
从数据和信号到交易策略
Strategy 对象包含您的交易逻辑,该逻辑根据 Cerebro 实例在回测执行期间在每个呈现的
data feed
信息进行订单交易。您可以通过将策略配置为任意参数来轻松测试
Cerebro
。
对于回测的每个柱线,Cerebro 实例调用 Strategy 实例的
.prenext()
或
.next()
方法。
.prenext()
的作用是解决尚未包含所有
data feed
的完整数据的柱线,例如,在有足够的时间来计算内置移动平均线等指标之前,或者是否存在其他缺失数据。默认情况下什么都不做,但如果您的主要策略旨在处理缺失值,您可以添加您选择的交易逻辑或调用
next()
。
您还可以使用 backtrader 而不定义显式策略,而是使用简化的信号接口。不过,策略 API 为您提供了更多的控制权和灵活性;有关如何使用 Signals API 的详细信息,请参阅 backtrader 文档。
输出订单的策略:接下来让我们看看 backtrader 如何处理这些订单。
一旦您的策略评估了每个柱线的当前和过去数据点,它需要决定下哪些订单。backtrader 允许您创建几种标准订单类型,Cerebro 将这些订单类型传递给 Broker 实例以供执行,并在每个柱线的
line
处提供结果通知。
您可以使用策略方法
buy()
和
sell()
来下市价单、平仓单和限价单,以及止损单和止损限价单。执行工作如下:
- 市价单:在下一个打开的柱线处成交
- 关闭订单:在下一个闭合柱线处成交
- 限价单:仅在达到价格阈值时才执行(例如,最多只买入 特定价格)在(可选)有效期内
- 止损单:如果价格达到给定阈值,则成为市价单
- 止损限价单:触发止损后变为限价单
在实践中,止损订单不同于限价订单,因为它们在价格触发之前无法被市场看到。backtrader 还提供计算所需规模的目标订单,考虑到当前头寸以实现一定的投资组合分配,如股份数量、头寸价值或投资组合价值的百分比。此外,对于多头订单,还有一些括号订单将买入与两个限价卖出订单结合在一起,这些订单在买入执行时激活。如果其中一个卖单成交或取消,另一个卖单也会取消。
经纪人处理订单执行、跟踪投资组合、现金价值和通知,并实施佣金和滑点等交易成本。如果没有足够的现金,经纪商可能会拒绝交易;对买卖进行排序以确保流动性可能很重要。backtrader 还具有
cheat\_on\_open
功能,允许展望下一根柱线,以避免由于下一根柱线的不利价格变动而拒绝交易。当然,此功能会使您的结果产生偏差。
除了像绝对交易价值的固定金额或百分比金额这样的佣金计划外,您还可以实现自己的逻辑,如下所示,每股固定费用。
Cerebro 控制系统根据时间戳表示的柱线同步
data feed
,并相应地在逐个事件的基础上运行交易逻辑和经纪人操作。backtrader 对频率或交易日历没有任何限制,可以并行使用多个时间范围。
如果它可以预加载源数据,它还可以矢量化指标的计算。您可以使用多个选项从内存角度优化操作(有关详细信息,请参阅 Cerebro 文档)。
如何在实践中使用 backtrader
我们将创建 Cerebro 实例、加载数据、制定和添加策略、运行回测并查看结果。
我们需要确保买卖股票的所有日期的价格信息,而不仅仅是有预测的日子。为了从 Pandas DataFrame 加载数据,我们继承 backtrader 的
PandasData
类来定义我们将提供的字段:
class SignalData(PandasData):
"""
Define pandas DataFrame structure
"""
cols = OHLCV + ['predicted']
# create lines
lines = tuple(cols)
# define parameters
params = {c: -1 for c in cols}
params.update({'datetime': None})
params = tuple(params.items())
然后我们实例化一个 Cerebro 类并使用 SignalData 类为我们从 HDF5 加载的数据集中的每个股票代码添加一个数据源:
cerebro = bt.Cerebro() # create a "Cerebro" instance
cash = 10000
# comminfo = FixedCommisionScheme()
# cerebro.broker.addcommissioninfo(comminfo)
cerebro.broker.setcash(cash)
idx = pd.IndexSlice
data = pd.read_hdf('00\_data/backtest.h5', 'data').sort_index()
tickers = data.index.get_level_values(0).unique()
for ticker in tickers:
df = data.loc[idx[ticker, :], :].droplevel('ticker', axis=0)
df.index.name = 'datetime'
bt_data = SignalData(dataname=df)
cerebro.adddata(bt_data, name=ticker)
现在,我们已准备好定义我们的策略。
如何制定交易逻辑
我们的
MLStrategy
子类化 backtrader 的 Strategy 类并定义我们可以用来修改其行为的参数。我们还创建了一个日志文件来创建交易记录:
class MLStrategy(bt.Strategy):
params = (('n\_positions', 10),
('min\_positions', 5),
('verbose', False),
('log\_file', 'backtest.csv'))
def log(self, txt, dt=None):
""" Logger for the strategy"""
dt = dt or self.datas[0].datetime.datetime(0)
with Path(self.p.log_file).open('a') as f:
log_writer = csv.writer(f)
log_writer.writerow([dt.isoformat()] + txt.split(','))
该策略的核心在于
.next()
方法。我们对具有最高正/最低负预测的
n\_position
股票进行做多/做空,只要至少有
min\_
个头寸。我们总是卖出没有出现在新的多头和空头列表中的任何现有头寸,并使用
order\_target\_percent
在新目标中建立等权重头寸:
def prenext(self):
self.next()
def next(self):
today = self.datas[0].datetime.date()
# if today.weekday() not in [0, 3]: # only trade on Mondays;
# return
positions = [d._name for d, pos in self.getpositions().items() if pos]
up, down = {}, {}
missing = not_missing = 0
for data in self.datas:
if data.datetime.date() == today:
if data.predicted[0] > 0:
up[data._name] = data.predicted[0]
elif data.predicted[0] < 0:
down[data._name] = data.predicted[0]
# sort dictionaries ascending/descending by value
# returns list of tuples
shorts = sorted(down, key=down.get)[:self.p.n_positions]
longs = sorted(up, key=up.get, reverse=True)[:self.p.n_positions]
n_shorts, n_longs = len(shorts), len(longs)
# only take positions if at least min\_n longs and shorts
if n_shorts < self.p.min_positions or n_longs < self.p.min_positions:
longs, shorts = [], []
for ticker in positions:
if ticker not in longs + shorts:
self.order_target_percent(data=ticker, target=0)
self.log(f'{ticker},CLOSING ORDER CREATED')
short_target = -1 / max(self.p.n_positions, n_shorts)
long_target = 1 / max(self.p.n_positions, n_longs)
for ticker in shorts:
self.order_target_percent(data=ticker, target=short_target)
self.log('{ticker},SHORT ORDER CREATED')
for ticker in longs:
self.order_target_percent(data=ticker, target=long_target)
self.log('{ticker},LONG ORDER CREATED')
现在,我们需要配置我们的 Cerebro 实例并添加我们的策略。
如何配置 Cerebro 实例
我们使用自定义佣金计划,假设对于购买或出售的份额,我们支付每笔固定金额 0.02 美元:
class FixedCommisionScheme(bt.CommInfoBase):
"""
Simple fixed commission scheme for demo
"""
params = (
('commission', .02),
('stocklike', True),
('commtype', bt.CommInfoBase.COMM_FIXED),
)
def _getcommission(self, size, price, pseudoexec):
return abs(size) * self.p.commission
然后,我们定义起始现金金额并相应地配置佣金:
cash = 10000
cerebro.broker.setcash(cash)
comminfo = FixedCommisionScheme()
cerebro.broker.addcommissioninfo(comminfo)
现在,缺少的只是将 MLStrategy 添加到我们的 Cerebro 实例,为所需的头寸数量和最少的多头/空头数量提供参数。我们还将添加一个 pyfolio 分析器:
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
cerebro.addstrategy(MLStrategy, n_positions=25, min_positions=20,
verbose=True, log_file='bt\_log.csv')
start = time()
results = cerebro.run()
ending_value = cerebro.broker.getvalue()
duration = time() - start
print(f'Final Portfolio Value: {ending\_value:,.2f}')
print(f'Duration: {format\_time(duration)}')
回测使用 869 个交易日,运行时间约为 45 秒。下图显示了累积回报和投资组合价值的演变,以及多头和空头头寸的每日价值。
性能看起来与之前的矢量化测试有些相似,上半年表现优于标准普尔 500 指数,之后表现不佳。
backtrader 是一个非常简单,但具有灵活且高性能特性的本地回测工具。 由于 Pandas 的兼容性,您可以从各种来源以您希望的频率加载任何数据集。 策略可以让您定义任意交易逻辑; 您只需要确保根据需要访问不同的数据源。 它还能与 pyfolio 很好地集成,以进行快速而全面的性能评估。
在演示中,我们将交易逻辑应用于预训练模型的预测。我们还可以在回测期间训练模型,因为我们可以访问当前柱线之前的数据。然而,通常将模型训练与策略选择分离并避免重复模型训练更有效。
E N D
扫描本文最下方二维码获取全部完整源码和Jupyter Notebook 文件打包下载。
↓**↓ 长按扫码获取完整源码↓↓******