Python量化投资回测利器:BT(Backtesting Toolkit)框架深度解析与实用教程

好的,这是一份关于 bt (Backtesting Toolkit) Python 库的详细解析和教程,内容使用中文编写,并包含目录。

bt 是一个灵活、基于 Python 的开源回测框架,旨在帮助量化交易员和研究人员快速测试和评估交易策略。它构建在 Pandas 和 NumPy 等标准库之上,易于集成和扩展。


目录

  1. 简介
  2. 1.1 什么是 bt?
  3. 1.2 为什么选择 bt?
  4. 安装
  5. 核心概念
  6. 3.1 数据 (Data)
  7. 3.2 算法 (Algorithm)
  8. 3.3 策略 (Strategy)
  9. 3.4 回测 (Backtest)
  10. 3.5 结果 (Result)
  11. 3.6 树状结构 (Tree Structure)
  12. 快速入门:一个简单的例子
  13. 4.1 获取数据
  14. 4.2 定义策略
  15. 4.3 创建并运行回测
  16. 4.4 查看结果
  17. 关键组件详解
  18. 5.1 数据处理
  19. 获取数据 (bt.get)
  20. 自定义数据
  21. 5.2 内置算法 (Algos)
  22. 时间驱动型 (Timing Algos)
  23. 选择型 (Selection Algos)
  24. 权重分配型 (Weighting Algos)
  25. 执行/再平衡型 (Execution/Rebalancing Algos)
  26. 其他辅助型
  27. 5.3 构建策略树
  28. 单个策略
  29. 嵌套策略 (父子策略)
  30. 5.4 配置回测 (Backtest)
  31. 初始资本
  32. 佣金和滑点
  33. 整数股数
  34. 5.5 结果分析 (Result)
  35. 常用统计指标 (display)
  36. 绘制图表 (plot)
  37. 获取详细数据
  38. 进阶主题
  39. 6.1 创建自定义算法
  40. 6.2 比较多个策略
  41. 6.3 处理更高频率的数据 (注意事项)
  42. 6.4 与其他库集成 (如 yfinance)
  43. 最佳实践与注意事项
  44. 7.1 数据质量
  45. 7.2 过拟合 (Overfitting)
  46. 7.3 前视偏差 (Lookahead Bias)
  47. 7.4 交易成本
  48. 7.5 现实性假设
  49. 总结

1. 简介

1.1 什么是 bt?

bt 是一个用于量化交易策略回测的 Python 库。它提供了一套结构化的工具,让用户可以方便地定义策略逻辑(何时买入/卖出、买入/卖出什么、买入/卖出多少),并使用历史数据模拟这些策略的执行情况,最终评估其表现。

1.2 为什么选择 bt?

  • 灵活性: bt 采用模块化设计,特别是其基于算法栈(Algo Stack)的策略构建方式,非常灵活。
  • 易用性: 对于熟悉 Pandas 的用户来说,上手相对容易。API 设计直观。
  • 集成性: 基于 Python 生态,可以方便地与 Pandas, NumPy, Matplotlib 等库协同工作。
  • 可扩展性: 用户可以方便地创建自定义算法来满足特定的策略需求。
  • 开源: 免费使用,社区活跃。
  • 2. 安装

    使用 pip 可以轻松安装 bt

    pip install bt
    

    建议同时安装一些常用的数据获取库(如 yfinance)和绘图库(matplotlibbt 会自动调用):

    pip install yfinance matplotlib
    

    3. 核心概念

    bt 的运作围绕以下几个核心概念构建:

    3.1 数据 (Data)

    回测的基础是历史市场数据。bt 主要使用 Pandas DataFrame 来存储时间序列数据,通常索引是日期时间(DatetimeIndex),列是不同的资产(如股票代码),值是价格(通常是收盘价)。

    3.2 算法 (Algorithm / Algo)

    算法是策略执行的具体逻辑单元。它们定义了交易决策的各个方面,例如:

  • 何时 触发交易或重新评估(例如,每月、每周、特定日期)。
  • 选择哪些 资产进行交易(例如,所有可用资产、表现最好的 N 个、满足特定条件的)。
  • 如何 分配权重(例如,等权重、市值加权、目标权重)。
  • 如何 执行交易(例如,再平衡到目标权重)。
  • 3.3 策略 (Strategy)

    策略是算法的容器和组织者。一个策略包含一个或多个算法,这些算法按照特定的顺序(栈)依次执行,共同定义了完整的交易逻辑。策略也可以包含子策略,形成一个树状结构。

    3.4 回测 (Backtest)

    回测是将策略应用于历史数据的过程。Backtest 对象负责管理这个模拟过程,包括跟踪投资组合价值、处理交易、计算费用等。

    3.5 结果 (Result)

    回测完成后,Result 对象存储了模拟的详细结果,包括每日的投资组合价值、交易记录、各种性能统计指标(如年化收益率、夏普比率、最大回撤等)以及绘图功能。

    3.6 树状结构 (Tree Structure)

    bt 的一个强大之处在于其策略可以组织成树状结构。一个父策略可以包含多个子策略。当回测运行时,数据和信号会从树的根节点流向叶节点,而权重等决策则可能从叶节点汇总回根节点。这使得构建复杂的多层策略(如资产配置策略下包含具体的选股子策略)成为可能。

    4. 快速入门:一个简单的例子

    让我们创建一个非常简单的策略:每月对 SPY 和 TLT(美国标普 500 ETF 和美国长期国债 ETF)进行等权重再平衡。

    import bt
    import pandas as pd
    import matplotlib.pyplot as plt
    
    # 设置 Matplotlib 在某些环境下正确显示图形
    %matplotlib inline
    
    # 4.1 获取数据
    # 使用 bt 自带的 get 函数获取雅虎财经数据
    # tickers = ['SPY', 'TLT']
    # start_date = '2010-01-01'
    # end_date = '2023-12-31'
    # data = bt.get(tickers, start=start_date, end=end_date)
    
    # 如果 bt.get 无法使用(有时会因雅虎财经接口变动),可以用 yfinance 替代
    import yfinance as yf
    tickers = ['SPY', 'TLT']
    start_date = '2010-01-01'
    end_date = '2023-12-31'
    data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']
    
    # 确保数据没有缺失值(简单处理:向前填充)
    data = data.ffill()
    
    print("数据预览:")
    print(data.head())
    
    # 4.2 定义策略
    # 策略名称: "EqualWeight_SPY_TLT"
    # 算法栈:
    # 1. RunMonthly(): 每月运行一次后续算法
    # 2. SelectAll(): 选择所有可用的资产 (SPY, TLT)
    # 3. WeighEqually(): 为选中的资产分配相等的权重
    # 4. Rebalance(): 根据目标权重调整仓位
    strategy = bt.Strategy('EqualWeight_SPY_TLT', [
        bt.algos.RunMonthly(),
        bt.algos.SelectAll(),
        bt.algos.WeighEqually(),
        bt.algos.Rebalance()
    ])
    
    # 4.3 创建并运行回测
    # 使用定义的策略和获取的数据创建回测对象
    backtest = bt.Backtest(strategy, data)
    
    # 运行回测
    # bt.run() 可以接受一个或多个回测对象
    results = bt.run(backtest)
    
    # 4.4 查看结果
    # 显示关键性能指标
    print("\n回测结果统计:")
    results.display()
    
    # 绘制权益曲线和资产权重图
    print("\n绘制图表:")
    results.plot(title='Equal Weight SPY & TLT Strategy')
    plt.show() # 确保图形显示
    
    # 绘制安全权重变化图
    results.plot_security_weights()
    plt.show()
    
    # 查看交易记录 (可选)
    # print("\n交易记录:")
    # print(results.get_transactions())
    

    这个例子展示了 bt 的基本流程:获取数据 -> 定义策略(通过组合算法)-> 创建回测 -> 运行回测 -> 分析结果。

    5. 关键组件详解

    5.1 数据处理

    获取数据 (bt.get)

    bt.get 是一个便捷函数,用于从雅虎财经等来源下载价格数据。

    # 获取多个 tickers 的数据
    data = bt.get('agg,eem,spy', start='2010-01-01')
    
    # 获取单个 ticker
    spy_data = bt.get('spy', start='2010-01-01', end='2022-12-31')
    

    注意: bt.get 的稳定性取决于底层数据源(如雅虎财经)API 的稳定性。有时可能需要使用 yfinancepandas_datareader 等其他库来获取数据。

    自定义数据

    任何符合格式(DatetimeIndex,列为资产,值为价格)的 Pandas DataFrame都可以用作 bt 的输入数据。

    # 假设你有一个 CSV 文件 my_data.csv
    # Date,AssetA,AssetB
    # 2020-01-01,100,50
    # 2020-01-02,101,51
    # ...
    
    # my_custom_data = pd.read_csv('my_data.csv', index_col='Date', parse_dates=True)
    # backtest = bt.Backtest(some_strategy, my_custom_data)
    # results = bt.run(backtest)
    

    重要提示: 确保数据质量!处理缺失值(NaN)非常重要,常见的处理方法包括向前填充 (ffill)、向后填充 (bfill) 或删除包含 NaN 的行/列(可能导致数据损失)。

    5.2 内置算法 (Algos)

    算法是 bt 的核心。它们像积木一样组合起来构建策略。算法通常按类型分类:

    时间驱动型 (Timing Algos)

    决定策略逻辑何时执行。

  • RunOnce(): 只在开始时运行一次。
  • RunDaily(): 每天运行。
  • RunWeekly(): 每周运行(默认周的最后一天)。
  • RunMonthly(): 每月运行(默认月最后一天)。
  • RunQuarterly(): 每季度运行。
  • RunYearly(): 每年运行。
  • RunOnDate(*dates): 在指定的日期运行。
  • RunAfterDays(days): 在策略启动 N 天后运行。
  • RunPeriodically(months=0, weeks=0, days=0): 按指定的周期运行。
  • 选择型 (Selection Algos)

    决定哪些资产被选中用于后续的权重分配。

  • SelectAll(): 选择当前数据中的所有资产。
  • SelectNone(): 不选择任何资产(用于清仓或作为逻辑分支)。
  • SelectThese(tickers): 选择指定的资产列表。
  • SelectN(n, sort_descending=True, all_or_none=False, filter_selected=None): 选择基于某种标准排序后的前 N 个或后 N 个资产(默认按价格排序,通常需要与其他算法如 SelectMomentum 结合)。
  • SelectWhere(signal, include_no_data=False): 基于一个布尔型的 DataFrame (signal) 来选择资产。signal DataFrame 的索引和列应与价格数据匹配,值为 True 的位置表示选中该资产。
  • SelectMomentum(n, lookback=pd.DateOffset(months=6), lag=pd.DateOffset(days=0), sort_descending=True): 选择动量(过去一段时间收益率)最高/最低的 N 个资产。
  • n: 选择的数量。
  • lookback: 计算动量的时间窗口。
  • lag: 动量计算与决策日之间的时间差(避免前视偏差)。
  • sort_descending: True 表示选动量最高的,False 表示选最低的。
  • 权重分配型 (Weighting Algos)

    为选中的资产分配目标权重。

  • WeighEqually(): 为所有选中的资产分配相等的权重。
  • WeighSpecified(**weights): 为指定的资产分配固定的权重,未指定的默认为 0。
  • WeighTarget(target_weights): 根据一个目标权重 DataFrame 或 Series 分配权重。这个 DataFrame/Series 的索引应是日期,列/索引应是资产名。这非常强大,可以实现复杂的动态权重逻辑。
  • WeighInvVol(lookback=pd.DateOffset(months=3), lag=pd.DateOffset(days=0)): 根据资产波动率的倒数分配权重(波动率越低,权重越高)。
  • WeighERC(lookback=pd.DateOffset(months=3), initial_weights=None, risk_weights=None, covar_method='ledoit-wolf', risk_parity_method='ccd', maximum_iterations=100, tolerance=1e-08): 实现等风险贡献(Equal Risk Contribution)组合。
  • 执行/再平衡型 (Execution/Rebalancing Algos)

    将计算出的目标权重转化为实际的持仓调整。

  • Rebalance(): 核心算法!它比较当前持仓和目标权重,生成交易指令以达到目标权重。几乎所有策略都需要这个算法放在算法栈的末尾。
  • CapitalFlow(amount): 在每次触发时向策略注入或提取固定金额的资金。
  • 其他辅助型
  • PrintDate(): 打印当前回测日期,用于调试。
  • PrintTempData(): 打印传递给下一个算法的临时数据 (temp),用于调试。
  • SelectHasData(min_period=1): 选择在当前时间点有有效数据的资产。
  • 算法栈 (Algo Stack):
    策略中的算法是按顺序执行的。前一个算法的输出(通常存储在 temp 变量中)会作为后一个算法的输入。例如,RunMonthly -> SelectMomentum(5) -> WeighEqually -> Rebalance 的流程是:

    1. RunMonthly: 检查今天是否是月末,如果是,则继续。
    2. SelectMomentum(5): 计算过去 N 个月的动量,选择动量最高的 5 个资产,并将列表放入 temp['selected']
    3. WeighEqually: 读取 temp['selected'] 中的资产列表,为它们计算等权重,并将目标权重放入 temp['weights']
    4. Rebalance: 读取 temp['weights'],与当前持仓比较,执行交易以匹配目标权重。

    5.3 构建策略树

    单个策略

    如快速入门所示,一个简单的策略就是一个 Strategy 对象,包含一个算法列表。

    sma_cross_strategy = bt.Strategy('SMACross', [
        bt.algos.RunMonthly(),
        # SelectWhere 需要一个布尔信号 DataFrame
        # (假设 signal_df 是预先计算好的买入信号)
        # bt.algos.SelectWhere(signal_df),
        bt.algos.WeighEqually(), # 对信号为 True 的资产等权重
        bt.algos.Rebalance()
    ])
    
    嵌套策略 (父子策略)

    可以将多个策略组合成一个父策略。这对于构建资产配置框架非常有用。

    # 子策略 1: 美国股票动量
    us_equity_momentum = bt.Strategy('USEquityMomentum', [
        bt.algos.RunMonthly(),
        bt.algos.SelectMomentum(n=3, lookback=pd.DateOffset(months=6)), # 选美股中动量前3
        bt.algos.WeighEqually(),
        bt.algos.Rebalance()
    ])
    
    # 子策略 2: 国际股票等权重
    intl_equity_equal = bt.Strategy('IntlEquityEqual', [
        bt.algos.RunMonthly(),
        bt.algos.SelectAll(), # 选择所有国际股票
        bt.algos.WeighEqually(),
        bt.algos.Rebalance()
    ])
    
    # 父策略: 资产配置 (60% 美股动量, 40% 国际股票)
    # 注意: 父策略的 SelectAll() 会将数据传递给子策略
    # 父策略的 WeighSpecified() 会将权重分配给子策略
    asset_allocation = bt.Strategy('AssetAllocation', [
        bt.algos.RunMonthly(),
        bt.algos.SelectAll(), # 确保子策略能接收到所有相关数据
        bt.algos.WeighSpecified(USEquityMomentum=0.6, IntlEquityEqual=0.4), # 分配权重给子策略
        bt.algos.Rebalance()
    ], children=[us_equity_momentum, intl_equity_equal]) # 指定子策略
    
    # 假设 all_data 包含了美国和国际股票的数据
    # us_tickers = ['SPY', 'QQQ', 'IWM']
    # intl_tickers = ['VEA', 'VWO', 'EEM']
    # all_tickers = us_tickers + intl_tickers
    # all_data = bt.get(all_tickers, start='2010-01-01')
    
    # 创建包含所有数据的 DataFrame (确保列名与子策略中的资产对应)
    # ... 准备好 all_data ...
    
    # 创建并运行回测 (使用父策略)
    # backtest_aa = bt.Backtest(asset_allocation, all_data)
    # results_aa = bt.run(backtest_aa)
    # results_aa.display()
    # results_aa.plot()
    

    在这个例子中:

    1. 父策略 AssetAllocation 每月运行。
    2. SelectAll 将所有资产数据传递下去。
    3. WeighSpecified 指定 USEquityMomentum 子策略获得 60% 的资本,IntlEquityEqual 子策略获得 40%。
    4. 数据和分配到的资本比例会传递给相应的子策略。
    5. 子策略内部执行各自的逻辑(选股、分配内部权重)。
    6. Rebalance 在父策略层面执行,确保整体分配比例和子策略内部的持仓都得到调整。

    5.4 配置回测 (Backtest)

    Backtest 对象用于将策略和数据结合起来进行模拟。

    backtest = bt.Backtest(
        strategy,
        data,
        name='MyBacktest',
        initial_capital=100000.0,
        commissions=lambda q, p: abs(q) * p * 0.001, # 0.1% 的佣金
        # slippage=... # 可以定义滑点函数
        integer_positions=False # 是否允许小数股数
    )
    
    初始资本

    initial_capital: 回测开始时的模拟资金,默认为 1,000,000。

    佣金和滑点

    commissions: 一个函数,接收交易数量 (q, 负数表示卖出) 和价格 (p),返回交易成本。
    slippage: 类似地,一个函数定义滑点模型。

    # 固定佣金示例: 每笔交易 5 美元
    def fixed_commission(q, p):
        return 5 if q != 0 else 0
    
    # 百分比佣金示例: 交易额的 0.1%
    def percentage_commission(q, p):
        return abs(q * p) * 0.001
    
    backtest_with_comm = bt.Backtest(
        strategy,
        data,
        commissions=percentage_commission
    )
    
    整数股数

    integer_positions: 默认为 True,表示只能交易整数股数。对于高价股或小额资本,这可能导致目标权重难以精确匹配。设为 False 允许小数股数,更接近某些现代券商的功能,但也可能不够现实。

    5.5 结果分析 (Result)

    bt.run() 返回一个 Result 对象(或者当运行多个回测时,返回一个包含多个 Result 对象的列表)。

    常用统计指标 (display)

    results.display() 打印出关键的性能指标,包括:

  • Total Return (总回报率)
  • CAGR (年化复合增长率)
  • Max Drawdown (最大回撤)
  • Volatility (年化波动率)
  • Sharpe Ratio (夏普比率)
  • Sortino Ratio (索提诺比率)
  • Calmar Ratio (卡玛比率)
  • Daily Value-at-Risk (每日风险价值)
  • 等等…
  • 绘制图表 (plot)

    results.plot() 绘制权益曲线、回撤、月度回报率等图表。
    results.plot_security_weights() 绘制资产权重随时间变化的图表。
    results.plot_histograms() 绘制收益率分布直方图。

    获取详细数据

    Result 对象还包含许多有用的属性和方法来获取底层数据:

  • results.prices: 策略的每日净值(类似价格)。
  • results.equity: 策略的每日总权益。
  • results.turnover: 策略的换手率。
  • results.stats: 包含 display() 中所有指标的 Pandas Series。
  • results.get_transactions(): 获取所有交易记录的 DataFrame。
  • results.get_security_weights(): 获取每日资产权重的 DataFrame。
  • # 获取年化收益率
    cagr = results.stats['cagr']
    print(f"年化复合增长率: {cagr:.2%}")
    
    # 获取最大回撤
    max_dd = results.stats['max_drawdown']
    print(f"最大回撤: {max_dd:.2%}")
    
    # 获取交易记录
    transactions = results.get_transactions()
    print("\n部分交易记录:")
    print(transactions.head())
    

    6. 进阶主题

    6.1 创建自定义算法

    如果内置算法无法满足需求,可以创建自己的算法。需要继承 bt.Algo 并实现 __call__ 方法。

    __call__(self, target) 方法是算法的核心。它接收一个 target 对象(通常是 StrategyBacktest 实例),并应返回 True 表示算法成功执行并希望继续执行后续算法,或 False 中断当前时间点的算法栈执行。

    自定义算法可以通过 target.temp 字典与其他算法交互(读取输入、写入输出),也可以访问 target.now (当前日期), target.data (原始数据), target.universe (当前可用资产数据), target.weights (当前持仓权重) 等属性。

    import bt
    import pandas as pd
    import numpy as np
    
    # 示例:自定义算法 - 只在星期一进行交易
    class RunOnMonday(bt.Algo):
        """
        只在星期一触发后续算法执行的 Algo。
        """
        def __init__(self):
            super(RunOnMonday, self).__init__()
    
        def __call__(self, target):
            # target.now 是当前回测的日期
            if target.now.weekday() == 0: # 0 代表星期一
                # 如果是星期一,返回 True,让后续算法执行
                return True
            else:
                # 如果不是星期一,返回 False,中断后续算法
                return False
    
    # 示例:自定义算法 - 基于外部信号文件分配权重
    class WeighFromSignalFile(bt.Algo):
        """
        根据外部 CSV 文件中的信号分配权重的 Algo。
        假设 CSV 文件格式: Date, AssetA_weight, AssetB_weight, ...
        """
        def __init__(self, signal_filepath):
            super(WeighFromSignalFile, self).__init__()
            # 在初始化时读取并处理信号文件
            self.signals = pd.read_csv(signal_filepath, index_col='Date', parse_dates=True)
            # 可能需要对齐索引和列名到主数据
    
        def __call__(self, target):
            # 获取当前日期的目标权重
            if target.now in self.signals.index:
                current_weights = self.signals.loc[target.now]
                # 过滤掉权重为 NaN 或 0 的资产
                current_weights = current_weights[current_weights > 0].dropna()
    
                # 将权重存入 target.temp['weights']
                target.temp['weights'] = current_weights
                return True
            else:
                # 如果当天没有信号,可以选择不操作或维持旧权重 (取决于逻辑)
                # 这里简单地不设置权重,后续 Rebalance 可能不会执行交易
                target.temp['weights'] = pd.Series(dtype=float) # 设置空权重
                return True # 仍然返回 True,让 Rebalance 处理空权重(即清仓或维持)
    
    # --- 使用自定义算法 ---
    # ticker_data = bt.get('SPY,AGG', start='2018-01-01')
    # my_strategy = bt.Strategy('MondayTrader', [
    #     RunOnMonday(),         # 使用自定义的时间算法
    #     bt.algos.SelectAll(),
    #     bt.algos.WeighEqually(), # 简单示例,可以在周一等权重
    #     bt.algos.Rebalance()
    # ])
    # backtest = bt.Backtest(my_strategy, ticker_data)
    # results = bt.run(backtest)
    # results.display()
    

    6.2 比较多个策略

    bt.run() 可以同时接收多个 Backtest 对象,方便直接比较它们的性能。

    # 策略 1: 等权重 (来自前面的例子)
    strategy1 = bt.Strategy('EqualWeight', [
        bt.algos.RunMonthly(),
        bt.algos.SelectAll(),
        bt.algos.WeighEqually(),
        bt.algos.Rebalance()
    ])
    backtest1 = bt.Backtest(strategy1, data) # 使用之前获取的 data
    
    # 策略 2: 只持有 SPY
    strategy2 = bt.Strategy('BuyAndHold_SPY', [
        bt.algos.RunOnce(),
        bt.algos.SelectThese(['SPY']), # 只选 SPY
        bt.algos.WeighSpecified(SPY=1.0), # 满仓 SPY
        bt.algos.Rebalance()
    ])
    backtest2 = bt.Backtest(strategy2, data)
    
    # 策略 3: 60/40 SPY/TLT
    strategy3 = bt.Strategy('60_40_SPY_TLT', [
        bt.algos.RunMonthly(),
        bt.algos.SelectAll(),
        bt.algos.WeighSpecified(SPY=0.6, TLT=0.4),
        bt.algos.Rebalance()
    ])
    backtest3 = bt.Backtest(strategy3, data)
    
    # 同时运行多个回测
    all_results = bt.run(backtest1, backtest2, backtest3)
    
    # 显示比较结果
    print("\n策略比较:")
    all_results.display()
    
    # 绘制比较图表
    all_results.plot(title='Strategy Comparison')
    plt.show()
    

    Result 对象现在会包含所有策略的表现,方便进行横向对比。

    6.3 处理更高频率的数据 (注意事项)

    bt 主要设计用于日线或更低频率(周线、月线)的回测。虽然理论上可以将更高频率(如小时线、分钟线)的数据输入 Pandas DataFrame 进行回测,但需要注意以下几点:

  • 性能: 对非常高频率的数据进行逐 bar 回测可能会非常慢。
  • 内置算法的适用性: bt 的很多内置算法(如 RunMonthly, SelectMomentum 的默认 lookback)是基于日线逻辑设计的。在高频场景下,你可能需要:
  • 创建自定义的时间算法 (RunEveryNMinutes, RunHourly 等)。
  • 调整或创建自定义的选择和权重算法,使用适合高频数据的时间窗口 (lookback, lag)。
  • 交易逻辑: 高频交易通常涉及更复杂的订单类型(市价单、限价单)、滑点模型和成交逻辑,bt 的默认 Rebalance 机制可能过于简化。你可能需要创建非常复杂的自定义执行算法。
  • 数据成本和质量: 高质量的高频数据通常获取成本高昂,且处理(清洗、对齐时间戳)更复杂。
  • 总的来说,对于严肃的高频回测,可能需要考虑使用更专门的框架(如 Zipline、Backtrader 的某些特性,或者专门的高频库)。但在较低频率的日内策略(例如,每天开盘/收盘交易一次)或小时级别策略,bt 配合自定义算法仍然是可行的。

    6.4 与其他库集成 (如 yfinance)

    bt 的优势在于其输入是标准的 Pandas DataFrame。这意味着你可以使用任何你喜欢的方式来获取和处理数据,只要最终能转换成 bt 期望的格式即可。

    import yfinance as yf
    import bt
    
    # 使用 yfinance 获取数据
    tickers = ['AAPL', 'MSFT', 'GOOG']
    start = '2015-01-01'
    end = '2023-12-31'
    yf_data = yf.download(tickers, start=start, end=end)
    
    # 选择需要的价格列,通常是 'Adj Close' (已调整收盘价)
    price_data = yf_data['Adj Close']
    
    # 处理缺失值 (重要!)
    price_data = price_data.ffill().dropna() # 先向前填充,再删除开头可能存在的 NaN
    
    # 创建策略
    tech_momentum_strategy = bt.Strategy('TechMomentum', [
        bt.algos.RunMonthly(),
        bt.algos.SelectMomentum(n=1, lookback=pd.DateOffset(months=6)), # 选择动量最好的 1 个
        bt.algos.WeighEqually(), # 只有一个,权重为 1
        bt.algos.Rebalance()
    ])
    
    # 创建并运行回测
    backtest_yf = bt.Backtest(tech_momentum_strategy, price_data)
    results_yf = bt.run(backtest_yf)
    
    # 显示结果
    results_yf.display()
    results_yf.plot()
    plt.show()
    

    7. 最佳实践与注意事项

    进行有效的回测需要遵循一些原则,避免常见的陷阱:

    7.1 数据质量

  • 准确性: 确保数据来源可靠,没有错误。
  • 完整性: 处理好缺失值(NaN)。
  • 调整: 使用经过股息和拆分调整后的价格(如 ‘Adj Close’)进行收益计算,除非你的策略明确基于未调整价格。
  • 幸存者偏差: 确保你的数据包含了历史上存在但后来退市的公司(如果适用),否则可能高估策略表现。
  • 7.2 过拟合 (Overfitting)

    策略在历史数据上表现极好,但在未来实盘中表现糟糕。这通常是因为策略过度拟合了历史数据的特定噪声或模式。

  • 避免数据窥视: 不要根据完整的历史数据来选择策略参数。
  • 样本外测试 (Out-of-Sample Testing): 将数据分为样本内(用于开发和优化策略)和样本外(用于验证策略)两部分。
  • 参数稳健性: 测试策略对参数微小变化的敏感度。稳健的策略在参数略有变动时表现不会急剧下降。
  • 简化策略: 过于复杂的策略更容易过拟合。
  • 7.3 前视偏差 (Lookahead Bias)

    在回测的某个时间点使用了未来的信息。这是最严重的错误之一。

  • 滞后 (Lag): 在使用需要计算周期的指标(如移动平均线、动量)时,确保计算窗口不包含当前决策点的数据。btlag 参数有助于此。例如,在 T 日决策,动量计算应基于 T-1 日或更早的数据。
  • 信号时间: 确保交易信号的产生时间严格早于交易执行时间。
  • 7.4 交易成本

  • 佣金和滑点: 务必在回测中包含对佣金和滑点的现实估计。忽略它们会严重高估策略表现,尤其是高换手率策略。
  • 冲击成本: 大量交易可能影响市场价格,这在简单回测中通常不被考虑,但在现实中可能很重要。
  • 7.5 现实性假设

  • 成交保证: 回测通常假设所有订单都能在计算出的价格(如收盘价)完全成交,现实中并非如此。
  • 流动性: 回测没有考虑某些资产在某些时候可能流动性不足,无法完成交易。
  • 资金限制: 回测中的初始资本和资金流动可能与现实约束不同。
  • 8. 总结

    bt 是一个强大而灵活的 Python 回测框架。它通过模块化的算法栈和策略树结构,使得用户能够相对容易地构建、测试和比较各种量化交易策略。其与 Pandas 的紧密集成简化了数据处理流程。

    要有效地使用 bt(以及任何回测工具),关键在于理解其核心概念,掌握常用算法的用法,并注意避免回测中的常见陷阱,如过拟合和前视偏差,同时纳入对交易成本和现实限制的考量。

    希望这份详细的解析和教程能帮助你开始使用 bt 进行量化策略的回测与研究。建议进一步探索 bt 的官方文档和示例以了解更多高级功能和细节。

    作者:hiquant

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python量化投资回测利器:BT(Backtesting Toolkit)框架深度解析与实用教程

    发表回复