用 backtrader 对算法交易进行量化回测

火山方舟向量数据库增长营销

picture.image

backtrader是基于Python的量化回测框架,优点是运行速度快,支持pandas的矢量运算;支持参数自动寻优运算,内置了talib股票分析技术指标库;支持多品种、多策略、多周期的回测和交易;支持pyfolio、empyrical分析模块库、alphalens多因子分析模块库等;扩展灵活,可以集成TensorFlow、PyTorch和Keras等机器学习、神经网络分析模块。

(扫描本文最下方二维码获取全部完整源码和Jupyter Notebook 文件打包下载。)

我们将首先总结 backtrader 的关键概念,以阐明在此平台上的回测工作流程,然后演示回测的使用方法,并通过机器学习进行预测。

Cerebro 架构的关键概念

backtrader 的 Cerebro(西班牙语为“大脑”)架构将回测工作流的关键组件表示为(可扩展的)Python 对象。这些对象相互作用以促进处理输入数据和因子计算、制定和执行策略、接收和执行订单以及跟踪和测量性能。Cerebro 调度从收集输入、逐条执行回测和提供结果的整个过程。

下图概述了 Cerebro 架构中的关键元素:

picture.image

Data feedslinesindicators

Data feeds 是策略的原材料,包含有关单个证券的信息,例如每次观察都带有时间戳的 OHLCV 市场数据,但您可以自定义可用字段。backtrader 可以从各种来源(包括 CSV 文件和 Pandas DataFrames)以及雅虎财经等在线来源获取数据。您还可以使用扩展程序连接到盈透证券等在线交易平台,以获取实时数据并执行交易。与 DataFrame 对象的兼容性意味着您可以从可访问的 Pandas 加载数据,范围从数据库到 HDF5 文件。

加载数据后,我们将 data feeds 添加到 Cerebro 实例,然后按照收到的顺序使其可用于一个或多个策略。您的策略的交易逻辑可以按名称(例如,股票代码)或序列号访问每个 data feed ,并检索 data feed 任何字段的当前和过去值。每个字段称为一行。

backtrader 带有 130 多个常用技术指标,让您可以根据每个 data feedline 或其他指标计算新值,以运行您的策略。您还可以使用标准 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 指数,之后表现不佳。

picture.image

backtrader 是一个非常简单,但具有灵活且高性能特性的本地回测工具。 由于 Pandas 的兼容性,您可以从各种来源以您希望的频率加载任何数据集。 策略可以让您定义任意交易逻辑; 您只需要确保根据需要访问不同的数据源。 它还能与 pyfolio 很好地集成,以进行快速而全面的性能评估。

在演示中,我们将交易逻辑应用于预训练模型的预测。我们还可以在回测期间训练模型,因为我们可以访问当前柱线之前的数据。然而,通常将模型训练与策略选择分离并避免重复模型训练更有效。

picture.image

E N D

picture.image

扫描本文最下方二维码获取全部完整源码和Jupyter Notebook 文件打包下载。

↓**↓ 长按扫码获取完整源码↓******

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动客户端性能优化最佳实践
在用户日益增长、需求不断迭代的背景下,如何保证 APP 发布的稳定性和用户良好的使用体验?本次分享将结合字节跳动内部应用的实践案例,介绍应用性能优化的更多方向,以及 APM 团队对应用性能监控建设的探索和思考。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论