Python的量化交易:如何使用`Zipline`和`Backtrader`进行交易策略回测。

Python量化交易:Zipline与Backtrader回测实战

各位朋友,大家好!今天我们来聊聊Python量化交易中两个非常重要的回测框架:Zipline和Backtrader。回测是量化交易策略开发过程中至关重要的一环,它能帮助我们评估策略在历史数据上的表现,从而指导我们优化策略,避免盲目投入实盘交易。

1. 回测的重要性与基本流程

在开始深入了解Zipline和Backtrader之前,我们先来明确一下回测的重要性。

为什么需要回测?

  • 验证策略有效性: 回测能够模拟策略在历史市场中的表现,检验策略是否能够产生预期的收益。
  • 风险评估: 通过回测,我们可以了解策略的最大回撤、波动率等风险指标,评估策略的风险承受能力。
  • 参数优化: 回测可以帮助我们找到策略的最优参数组合,提高策略的收益率。
  • 避免过拟合: 回测可以帮助我们检测策略是否存在过拟合现象,即策略在历史数据上表现很好,但在未来市场中表现不佳。

回测的基本流程:

  1. 数据准备: 收集历史交易数据,包括股票代码、时间、开盘价、最高价、最低价、收盘价、成交量等。
  2. 策略编写: 根据交易逻辑编写交易策略的代码。
  3. 回测引擎配置: 配置回测引擎的参数,包括起始资金、交易手续费、滑点等。
  4. 运行回测: 运行回测引擎,模拟策略在历史数据上的交易过程。
  5. 结果分析: 分析回测结果,包括收益率、最大回撤、夏普比率等指标,评估策略的性能。

2. Zipline:事件驱动的回测框架

Zipline 是由 Quantopian 开发的一个开源的回测框架,它以事件驱动的方式模拟交易过程。Zipline 的特点是易于使用、性能高效、支持自定义数据源和交易规则。

2.1 Zipline 的基本概念

  • Algorithm: 算法是 Zipline 的核心,它包含了策略的交易逻辑。算法定义了 initialize()handle_data()before_trading_start() 三个重要的函数。

    • initialize(context): 在回测开始时执行一次,用于初始化算法的参数和状态。context 对象用于存储算法的全局变量。
    • handle_data(context, data): 在每个交易日执行一次,用于处理当前时刻的市场数据,并根据策略逻辑进行交易。data 对象包含了当前时刻的市场数据。
    • before_trading_start(context, data): 在每个交易日开盘前执行一次,用于进行一些准备工作,例如计算移动平均线等。
  • DataPortal: 数据门户负责提供历史数据给算法。Zipline 支持多种数据源,例如 CSV 文件、数据库等。

  • Order: 订单是算法发出的交易指令。Zipline 支持多种订单类型,例如市价单、限价单等。

  • Portfolio: 投资组合记录了算法持有的资产和现金。

  • PerfTracker: 性能跟踪器负责记录回测过程中的各种指标,例如收益率、最大回撤等。

2.2 Zipline 的安装与配置

Zipline 的安装相对复杂,因为它依赖于许多 Python 包。建议使用 Anaconda 环境来安装 Zipline。

conda create -n zipline python=3.8
conda activate zipline
conda install -c conda-forge zipline

安装完成后,需要下载一些示例数据:

zipline ingest

2.3 Zipline 代码示例:均线策略

下面是一个简单的均线策略的 Zipline 代码示例:

import zipline
from zipline.api import order_target, record, symbol
from zipline.utils.events import date_rules, time_rules
import pandas as pd

def initialize(context):
    """
    初始化策略
    """
    context.i = 0
    context.asset = symbol('AAPL')
    context.sma_length = 20

    # 按月运行 before_trading_start 函数
    schedule_function(
        rebalance,
        date_rules.month_start(),
        time_rules.market_open(),
    )

    # 每天记录价格和移动平均线
    schedule_function(
        record_vars,
        date_rules.every_day(),
        time_rules.market_close(),
    )

def handle_data(context, data):
    """
    处理每个交易日的数据
    """
    context.i += 1
    if context.i < context.sma_length:
        return

    sma = data.history(context.asset, 'price', bar_count=context.sma_length, frequency="1d").mean()

    if data.current(context.asset, 'price') > sma:
        order_target(context.asset, 100)
    elif data.current(context.asset, 'price') < sma:
        order_target(context.asset, 0)

    record(AAPL=data.current(context.asset, 'price'), SMA=sma)

def rebalance(context, data):
    """
    每月初重新平衡仓位
    """
    pass  # 在这个例子中,我们不做任何操作

def record_vars(context, data):
    """
    记录变量
    """
    pass # 这个例子中,handle_data 已经进行记录了

代码解释:

  • initialize(context) 函数初始化了策略的参数,包括股票代码 AAPL 和移动平均线的长度 20。同时,使用 schedule_function 函数安排了 rebalancerecord_vars 函数的执行时间。
  • handle_data(context, data) 函数是策略的核心,它在每个交易日执行。该函数首先计算过去 20 天的移动平均线,然后根据当前价格与移动平均线的关系来决定是否买入或卖出股票。
  • order_target(context.asset, 100) 函数表示将 AAPL 的仓位调整到 100 股。
  • record(AAPL=data.current(context.asset, 'price'), SMA=sma) 函数用于记录当前价格和移动平均线。

2.4 运行 Zipline 回测

要运行 Zipline 回测,需要创建一个 Python 脚本,并调用 zipline.run_algorithm() 函数。

import pandas as pd
from zipline import run_algorithm
from zipline.api import symbol
from datetime import datetime
import pytz
from moving_average_cross import initialize, handle_data  # 导入你的策略文件

start = pd.Timestamp('2017-01-01', tz='utc')
end = pd.Timestamp('2018-01-01', tz='utc')

# 准备数据,这里需要创建一个 CSV 文件,包含 AAPL 的历史数据
# 数据需要包含 date, open, high, low, close, volume, adj_close 列
# 并将数据保存到 'AAPL.csv' 文件中

# 创建一个 DataFrame,用于存储数据
data = pd.read_csv('AAPL.csv', index_col='date', parse_dates=True)
data.index = data.index.tz_localize('UTC') # 设置时区

# 创建一个 Panel,用于存储数据
# panel = {
#     'AAPL': data
# }
# panel = pd.Panel(panel)

def load_data():

    # 加载数据
    data = pd.read_csv('AAPL.csv', index_col='date', parse_dates=True)
    data.index = data.index.tz_localize('UTC')  # 设置时区
    # 将数据转换为 Zipline 期望的格式
    return data

def analyze(context, perf):
    import matplotlib.pyplot as plt
    fig = plt.figure()
    ax1 = fig.add_subplot(111)
    perf.portfolio_value.plot(ax=ax1)
    ax1.set_ylabel('Portfolio value (USD)')
    plt.show()

if __name__ == '__main__':
    data = load_data()

    results = run_algorithm(
        start=start,
        end=end,
        initialize=initialize,
        handle_data=handle_data,
        capital_base=10000,
        data_frequency='daily',
        data=data
        # ,bundle_name = None  # 如果你使用了 `zipline ingest` 则需要注释掉这行
    )

    print(results)

    # 运行分析函数
    analyze(None, results)

运行上面的脚本,即可得到回测结果。 你需要将上面的 moving_average_cross.py 替换成你保存策略的文件名。

2.5 Zipline 的优缺点

优点:

  • 事件驱动: Zipline 以事件驱动的方式模拟交易过程,能够更真实地反映市场情况。
  • 易于使用: Zipline 提供了简单的 API,方便用户编写和运行回测。
  • 性能高效: Zipline 使用 Cython 编写,性能非常高效。
  • 支持自定义数据源和交易规则: Zipline 允许用户自定义数据源和交易规则,满足不同的需求。

缺点:

  • 安装复杂: Zipline 的安装相对复杂,因为它依赖于许多 Python 包。
  • 数据格式要求严格: Zipline 对数据格式有严格的要求,需要将数据转换为特定的格式才能使用。
  • 文档不够完善: Zipline 的文档不够完善,学习曲线较陡峭。
  • 不再积极维护: Zipline 项目的维护力度已经减弱,社区活跃度不高。

3. Backtrader:灵活的面向对象回测框架

Backtrader 是一个灵活的 Python 回测框架,它以面向对象的方式构建交易策略。Backtrader 的特点是易于扩展、支持多种数据源和交易规则、提供丰富的分析工具。

3.1 Backtrader 的基本概念

  • Strategy: 策略是 Backtrader 的核心,它包含了策略的交易逻辑。策略定义了 __init__()next()notify_order() 等重要的方法。

    • __init__(self): 策略的构造函数,用于初始化策略的参数和状态。
    • next(self): 在每个交易日执行一次,用于处理当前时刻的市场数据,并根据策略逻辑进行交易。
    • notify_order(self, order): 当订单状态发生变化时执行,例如订单被接受、拒绝、成交等。
  • Data Feed: 数据源负责提供历史数据给策略。Backtrader 支持多种数据源,例如 CSV 文件、数据库、Yahoo Finance 等。

  • Order: 订单是策略发出的交易指令。Backtrader 支持多种订单类型,例如市价单、限价单等。

  • Broker: 经纪人负责处理订单,并模拟交易过程。

  • Analyzer: 分析器负责计算回测过程中的各种指标,例如收益率、最大回撤等。

3.2 Backtrader 的安装

Backtrader 的安装非常简单,只需要使用 pip 命令即可:

pip install backtrader

3.3 Backtrader 代码示例:均线策略

下面是一个简单的均线策略的 Backtrader 代码示例:

import backtrader as bt

class MovingAverageStrategy(bt.Strategy):
    params = (
        ('period', 20),  # 移动平均线周期
        ('printlog', True),
    )

    def log(self, txt, dt=None, doprint=False):
        ''' Logging function for this strategy'''
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # 保存收盘价的引用
        self.dataclose = self.datas[0].close

        # 跟踪待处理的订单/买入价/手续费
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # 添加移动平均线指标
        self.sma = bt.indicators.SimpleMovingAverage(
            self.datas[0], period=self.params.period)

        # Backtrader 自带的平均真实波幅ATR指标
        self.atr = bt.indicators.ATR(self.datas[0], period=14)

    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

        # 检查订单是否完成
        # 注意: broker 可能会拒绝订单
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log(
                    'SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))

    def next(self):
        # 记录收盘价
        self.log('Close, %.2f' % self.dataclose[0])

        # 检查是否有未完成的订单
        if self.order:
            return

        # 检查是否已经在市场中
        if not self.position:

            # 当前价格大于移动平均线,则买入
            if self.dataclose[0] > self.sma[0]:

                # 买入
                self.log('BUY CREATE, %.2f' % self.dataclose[0])

                # 跟踪订单避免重复
                self.order = self.buy()

        else:

            # 当前价格小于移动平均线,则卖出
            if self.dataclose[0] < self.sma[0]:
                # 卖出
                self.log('SELL CREATE, %.2f' % self.dataclose[0])

                # 跟踪订单避免重复
                self.order = self.sell()

代码解释:

  • MovingAverageStrategy(bt.Strategy) 定义了一个继承自 bt.Strategy 的策略类。
  • __init__(self) 函数初始化了策略的参数,包括移动平均线的周期 period
  • next(self) 函数是策略的核心,它在每个交易日执行。该函数首先判断当前是否有未完成的订单,然后判断是否已经在市场中。如果不在市场中,且当前价格大于移动平均线,则买入股票。如果在市场中,且当前价格小于移动平均线,则卖出股票。
  • self.buy()self.sell() 函数用于发出买入和卖出订单。

3.4 运行 Backtrader 回测

要运行 Backtrader 回测,需要创建一个 Cerebro 对象,并添加策略、数据源和分析器。

import backtrader as bt
import datetime
import pandas as pd
from moving_average_strategy import MovingAverageStrategy  # 导入你的策略文件

if __name__ == '__main__':
    # 创建 Cerebro 引擎
    cerebro = bt.Cerebro()

    # 添加策略
    cerebro.addstrategy(MovingAverageStrategy)

    # 加载数据
    # data = bt.feeds.YahooFinanceCSVData(
    #     dataname='AAPL.csv',
    #     fromdate=datetime.datetime(2017, 1, 1),
    #     todate=datetime.datetime(2018, 1, 1),
    #     reverse=False)

    # 用 PandasData 加载数据
    df = pd.read_csv('AAPL.csv', index_col='date', parse_dates=True)
    data = bt.feeds.PandasData(dataname=df, fromdate=datetime.datetime(2017, 1, 1), todate=datetime.datetime(2018, 1, 1))

    cerebro.adddata(data)

    # 设置初始资金
    cerebro.broker.setcash(100000.0)

    # 设置手续费
    cerebro.broker.setcommission(commission=0.001)

    # 添加分析器
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')

    # 打印初始资金
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 运行回测
    results = cerebro.run()

    # 打印最终资金
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # 打印分析结果
    print('Returns:', results[0].analyzers.returns.get_analysis())
    print('Sharpe Ratio:', results[0].analyzers.sharpe.get_analysis())
    print('DrawDown:', results[0].analyzers.drawdown.get_analysis())

    # 绘制回测结果
    cerebro.plot()

代码解释:

  • cerebro = bt.Cerebro() 创建了一个 Cerebro 对象,它是 Backtrader 的核心引擎。
  • cerebro.addstrategy(MovingAverageStrategy) 将策略添加到引擎中。
  • cerebro.adddata(data) 将数据源添加到引擎中。 这里使用了 PandasData 避免了数据格式的问题。
  • cerebro.broker.setcash(100000.0) 设置初始资金为 100000.0。
  • cerebro.broker.setcommission(commission=0.001) 设置手续费为 0.001。
  • cerebro.addanalyzer(bt.analyzers.Returns, _name='returns') 添加收益率分析器。
  • cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe') 添加夏普比率分析器。
  • cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown') 添加回撤分析器。
  • cerebro.run() 运行回测。
  • cerebro.plot() 绘制回测结果。

运行上面的脚本,即可得到回测结果,包括收益率、夏普比率、最大回撤等指标,以及回测曲线。 需要将上面的 moving_average_strategy.py 替换成你保存策略的文件名。

3.5 Backtrader 的优缺点

优点:

  • 灵活易扩展: Backtrader 以面向对象的方式构建交易策略,易于扩展和定制。
  • 支持多种数据源和交易规则: Backtrader 支持多种数据源和交易规则,满足不同的需求。
  • 提供丰富的分析工具: Backtrader 提供了丰富的分析工具,方便用户评估策略的性能。
  • 文档完善: Backtrader 的文档非常完善,学习曲线较平缓。
  • 社区活跃: Backtrader 的社区非常活跃,用户可以获得及时的支持。

缺点:

  • 性能相对较低: 相比 Zipline,Backtrader 的性能相对较低,尤其是在处理大量数据时。
  • 代码量相对较多: 相比 Zipline,Backtrader 的代码量相对较多,需要编写更多的代码才能实现相同的功能。

4. Zipline 与 Backtrader 的比较

特性 Zipline Backtrader
编程范式 事件驱动 面向对象
易用性 相对简单,API 简洁 灵活,但需要编写更多代码
性能 高效,使用 Cython 编写 相对较低
数据源 支持多种数据源,但需要转换为特定格式 支持多种数据源,例如 CSV、数据库、Yahoo Finance 等
分析工具 相对较少 丰富,提供多种分析器
文档 不够完善,学习曲线较陡峭 完善,学习曲线较平缓
社区 活跃度不高 活跃
维护 维护力度减弱 积极维护
安装 相对复杂 简单

5. 如何选择合适的回测框架

选择合适的回测框架取决于你的具体需求和偏好。

  • 如果你追求性能,并且对数据格式有较强的控制力,可以选择 Zipline。
  • 如果你需要灵活易扩展的框架,并且希望使用面向对象的方式构建交易策略,可以选择 Backtrader。
  • 如果你的策略比较简单,并且对性能要求不高,可以选择 Backtrader。
  • 如果你的策略比较复杂,并且需要处理大量数据,可以选择 Zipline。

6. 回测的注意事项

  • 数据质量: 回测结果的准确性取决于数据的质量。需要确保数据是准确、完整和可靠的。
  • 手续费和滑点: 回测时需要考虑手续费和滑点的影响。手续费是指交易时需要支付的费用,滑点是指实际成交价格与预期价格之间的差异。
  • 流动性: 回测时需要考虑市场的流动性。如果市场流动性不足,可能会导致订单无法成交或成交价格不利。
  • 过拟合: 回测时需要注意避免过拟合。过拟合是指策略在历史数据上表现很好,但在未来市场中表现不佳。
  • 样本外测试: 为了验证策略的有效性,需要在样本外数据上进行测试。样本外数据是指没有用于训练策略的数据。

7. 结束语:回测是量化交易成功的基石

回测是量化交易策略开发过程中不可或缺的一环。通过回测,我们可以验证策略的有效性,评估策略的风险,并优化策略的参数。Zipline 和 Backtrader 是两个非常流行的 Python 回测框架,它们各有优缺点,可以根据自己的需求选择合适的框架。希望今天的分享能够帮助大家更好地理解和使用 Zipline 和 Backtrader,开发出更加优秀的量化交易策略。

回测框架选择因需求而异,回测过程需谨慎对待。

量化交易成功的基石是回测,希望今天的内容能帮助大家。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注