Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

如何用自助法构建回测置信区间

一条权益曲线只是一个样本。学习如何对回测夏普比率进行自助法重采样,构建并解读置信区间,判断结果是否可能只是历史行情造成的。

回测置信区间回答一个问题:一条权益曲线中,有多少来自策略本身,又有多少来自所选取的那段历史行情。对回测进行自助法重采样,是通过多次重复抽取收益序列来构建该区间;每次重采样后重新计算统计量,再读取结果分布的百分位数。基于单年日度观测计算出的夏普比率为 1.4,但其 95% 置信区间可能宽到包含零值。下方各面板将说明原因。

为什么一条权益曲线只是一个样本

回测会为每项统计指标给出一个数值:一个夏普比率、一个年化收益率、一个最大回撤、一个胜率。这些都不是策略的真实价值。每个数值都是基于有限交易日样本得出的估计值。若取相同长度的另一段时期,结果可能完全不同。

下表完全排除了策略因素。它衡量的是最简单的持仓方式:每次持有 SPY 一个日历年,依据收盘价到收盘价的价格回报计算。

查询每次持有一个仓位一年:SPY按日历年计算的年化夏普比率
每个数字背后的完整 SQL
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2011-12-01'
      AND date <  '2026-01-01'
)
SELECT
    toString(toYear(date))                            AS year,
    count()                                           AS obs_count,
    round(avg(ret) * 252 * 100, 2)                    AS ann_return_pct,
    round(stddevSamp(ret) * sqrt(252) * 100, 2)       AS ann_vol_pct,
    round(avg(ret) / stddevSamp(ret) * sqrt(252), 2)  AS sharpe_ratio
FROM daily
WHERE date >= '2012-01-01'
  AND ret IS NOT NULL
GROUP BY year
ORDER BY year
Run this yourself

每一行覆盖约 250 个交易日,接近标准交易年度。在这些年份中,持仓方式没有任何变化。在 2012,年化夏普比率为 1.06;在 2025,则为 0.88。图表共绘制了 14 年。仅持有广泛指数一年,核心指标就可能出现大多数读者不会视为噪声的幅度变化。因此,长度相同的策略回测至少也会承受同等程度的不确定性。该列所采用的年化方法见我们的夏普比率指南,区间收益的计算方法见月度收益如何计算

回测夏普比率的波动区间有多宽?

夏普比率估计值的标准误差,大致会随着观测数量的平方根增加而下降。与其依赖这一公式,不如直接测量分布范围。将二十年的交易日划分为固定长度、彼此不重叠的窗口,计算每个窗口内的年化夏普比率,再观察这些数值之间的差距。

查询SPY非重叠窗口的夏普比率离散度(2006至2025年)
每个数字背后的完整 SQL
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2005-12-01'
      AND date <  '2026-01-01'
),
indexed AS
(
    SELECT
        ret,
        row_number() OVER (ORDER BY date) AS i
    FROM daily
    WHERE date >= '2006-01-01'
      AND ret IS NOT NULL
),
blocks AS
(
    SELECT
        w,
        intDiv(i, w)                                            AS blk,
        count()                                                 AS n,
        avg(ret) / nullIf(stddevSamp(ret), 0) * sqrt(252)        AS sharpe
    FROM indexed
    CROSS JOIN (SELECT arrayJoin([21, 63, 126, 252, 504]) AS w) AS ws
    GROUP BY w, blk
    HAVING n = w
)
SELECT
    concat(toString(w), ' sessions')                          AS horizon,
    count()                                                   AS block_count,
    round(quantileDeterministic(0.05)(sharpe, blk), 2)        AS sharpe_p05,
    round(quantileDeterministic(0.50)(sharpe, blk), 2)        AS sharpe_p50,
    round(quantileDeterministic(0.95)(sharpe, blk), 2)        AS sharpe_p95,
    round(quantileDeterministic(0.95)(sharpe, blk)
        - quantileDeterministic(0.05)(sharpe, blk), 2)        AS sharpe_band_width
FROM blocks
WHERE sharpe IS NOT NULL
GROUP BY w
ORDER BY w
Run this yourself

每个窗口包含 21 sessions 个交易日时,实测夏普比率的第5至第95百分位数范围为 -3.376.97,在 238 个窗口中相差 10.34 个夏普点。将每个窗口延长至 504 sessions 个交易日后,差距降至 1.67 个点,但此时仅基于 8 个窗口进行测量。这里同时发生了两件事。样本量增加后,估计值更加精确;但历史数据能够提供的独立样本数量也会大幅减少。这种张力正是本文的核心。

如何对回测置信区间进行自助法估计

这一流程足够简短,可以完整列出。

  1. 从策略的实际收益率序列开始。每个期间记录一个数,共有 N 个。
  2. 从该列表中随机抽取 N 个收益率,并允许重复抽取。有些收益率会出现两次,有些一次也不会出现。
  3. 在这组重抽样数据上重新计算统计量。
  4. 重复数千次,并保留每次计算结果。
  5. 将保存的结果排序,读取第 2.5 个百分位和第 97.5 个百分位,得到 95% 置信区间。

在第 2 步之前设定随机数生成器的种子,并将种子记录在发布的区间旁边。自助法是一种蒙特卡洛估计:两次未设定种子的运行结果在末位数字上会有所不同;如果审阅者无法重新运行您的区间,就无法核验结果。这与可复现的回测设置中介绍的规范相同。

平均收益率和夏普比率可以顺利执行第 2 步,因为二者都将收益率列表视为一个集合。最大回撤则不同。回撤取决于路径顺序。重抽样会重新排列收益率,所得最大回撤可能并不存在于真实交易序列的任何排列中。只要明确标注结果含义,对最大回撤进行自助法估计仍然有价值:它反映的是重排历史中的回撤分布,而不是对下一次回撤的预测。定义见最大回撤

为什么 IID 自助法不适用于市场收益

第二步假设每个收益都相互独立且同分布,即 IID 假设。日收益会以一种改变区间宽度的方式打破这一假设。带符号的收益只保留很弱的单日记忆。但收益的绝对值会呈现聚集性:大幅波动往往相邻出现,平静交易日也会连续出现。

查询一阶滞后自相关:有符号收益率与绝对收益率(2016至2025年)
每个数字背后的完整 SQL
WITH daily AS
(
    SELECT
        ticker,
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('SPY', 'MSFT', 'KO', 'XOM', 'JNJ', 'PG')
      AND date >= '2015-11-01'
      AND date <  '2026-01-01'
),
lagged AS
(
    SELECT
        ticker,
        date,
        ret,
        lagInFrame(ret, 1) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS ret_prev
    FROM daily
    WHERE ret IS NOT NULL
      AND abs(ret) < 0.35
)
SELECT
    ticker,
    count()                                AS obs_count,
    round(corr(ret, ret_prev), 3)          AS return_autocorr,
    round(corr(abs(ret), abs(ret_prev)), 3) AS abs_return_autocorr
FROM lagged
WHERE date >= '2016-01-01'
  AND ret_prev IS NOT NULL
GROUP BY ticker
ORDER BY abs_return_autocorr DESC
Run this yourself

对于 SPY,在 2514个交易日中测得的日收益绝对值一阶滞后自相关为 0.366;同一交易日的带符号收益对应数值为 -0.133。比较图中任一名称对应的两根柱。打乱收益序列会破坏这种聚集性。对这些数据运行 IID 自助法,会得到比样本所支持的更窄区间。误差偏向最不利的方向:它美化了策略表现。

移动区块Bootstrap与区块长度的选择

解决方法是重新抽取连续区块,而不是单个收益率。先确定区块长度 L,再随机、有放回地抽取由 L 个连续收益率组成的区块,并将这些区块首尾拼接,直到合成序列达到 N 个观测值。区块内部的相关性得以保留,收益率仍按原有顺序排列。只有区块之间的连接部分是人为形成的。

区块长度需要在两类误差之间权衡,无法同时规避二者。较短的区块接近 IID Bootstrap,会低估区间宽度,形成偏差。较长的区块能保留更多相关性,但可供抽取的不同区块数量较少。因此,每次重抽样都会重复历史中的大段数据,区间估计本身也会变得嘈杂,形成方差。已发表的经验规则通常让区块长度与 N 的三分之一次方成比例。应将其视为起始点。

一种成本更低的诊断方法,是检查方差如何随期限变化。在独立条件下,k 日收益率总和的方差等于单日方差的 k 倍,两者之比为 1。

查询按区块长度划分的方差比率:SPY方差是否像独立抽样一样扩展?
每个数字背后的完整 SQL
WITH daily AS
(
    SELECT
        date,
        toFloat64(close) / nullIf(lagInFrame(toFloat64(close), 1)
            OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 0) - 1 AS ret
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= '2005-12-01'
      AND date <  '2026-01-01'
),
indexed AS
(
    SELECT
        ret,
        row_number() OVER (ORDER BY date) AS i
    FROM daily
    WHERE date >= '2006-01-01'
      AND ret IS NOT NULL
),
base AS
(
    SELECT varSamp(ret) AS var_1d FROM indexed
),
blocks AS
(
    SELECT
        k,
        intDiv(i, k)  AS blk,
        count()       AS n,
        sum(ret)      AS block_ret
    FROM indexed
    CROSS JOIN (SELECT arrayJoin([2, 3, 5, 10, 21, 42, 63]) AS k) AS ks
    GROUP BY k, blk
    HAVING n = k
)
SELECT
    concat(toString(k), ' sessions')                          AS block_length,
    count()                                                   AS block_count,
    round(varSamp(block_ret) / (k * any(var_1d)), 3)          AS variance_ratio
FROM blocks
CROSS JOIN base
GROUP BY k
ORDER BY k
Run this yourself

2 sessions,该比率为 0.844;在 63 sessions,该比率为 0.584。计算基于 78 个不重叠区块。接近 1 的数值表示,在该期限上,总和的扩展方式与独立抽样相同。偏离 1 的数值表示相关性在该期限上仍然发挥作用,这也是区块长度需要覆盖的范围。选择能够覆盖该范围的最短区块,然后再观察区块数量降至多少。

分桶重采样,以及每个区间旁的样本数

按波动率状态或日历月份进行分桶重采样,可以保留使问题具有分析价值的条件。如果某项主张是:策略在高波动率状态下获得其夏普比率,那么仅从高波动率交易日中抽样构建区间,才是真正检验该主张的方法。代价在于计算精度。将250个观测值分成四个桶后,每个桶约有60个观测值;基于60个观测值进行自助法重采样,得到的区间大约是全样本版本的两倍宽。

因此,每次都应在每个区间旁报告对应分桶的样本数。上方各面板中的block_count列直观体现了这一做法:从九个窗口中读出的第5百分位数,与从两百个窗口中读出的第5百分位数,是不同的统计对象,即使两者显示到小数点后两位相同。

自助法无法修复的问题

自助法只量化一件事:根据现有数据计算出的统计量所包含的抽样噪声。它无法判断这些数据是否曾经可以实际获得。

包含前视偏差的回测会生成一组从未能够交易的收益序列。对这组数据进行自助法重采样,只会围绕虚构结果给出一个紧窄且看似可靠的区间。若使用当前指数成分股构建股票池,就会带入幸存者偏差,对该股票池进行的每次重采样也都会继承这种偏差。第三个问题是选择效应:运行200个变体后保留表现最佳的一个,其自助法区间描述的只是该变体的抽样噪声,却忽略了用于筛选它的另外199次结果。诚实的区间估计很有用,但不能替代样本外数据。

数据说明与方法
  • 收益率采用日线数据的收盘价至收盘价收益率。不计股息,因此所有收益率和夏普比率的水平都会被低估,幅度大致相当于股息率。本文讨论的离散程度基本不受影响。
  • 窗口和区块互不重叠,因此每个统计量都使用历史数据中彼此不重叠的一段。使用重叠窗口会夸大样本数量。
  • 分位数采用确定性估计方法,因此重新运行面板时会返回相同的百分位数,而不是新的近似值。
  • 自相关面板使用六只大型股票,窗口内不存在拆股,并剔除涨跌幅超过35%的交易日,以避免价格调整造成的伪相关。

常见问题

Bootstrap置信区间能说明回测的什么问题?

它给出了这样一个范围:如果在相同的期数内再次按同一过程进行抽样,该统计量合理情况下可能落入其中。若95%置信区间包含零,说明样本期过短,无法将该策略与完全没有超额优势区分开来。

Bootstrap重抽样多少次才足够?

对于95%置信区间,几千次重抽样通常就能得到稳定结果,10,000次是常见默认值,计算成本也不高。要估计更极端的尾部分位数,则需要更多重抽样:在1,000次抽样中,第1百分位数大约只对应十个数值的位置。

移动区块Bootstrap应采用多长的区块?

不存在普遍适用的正确长度。经验法则通常将区块长度设为样本量的三次方根。一个实用的检验方法是观察方差不再线性扩展时对应的时间跨度;上方的方差比率面板对此进行了衡量。请在发布置信区间时,同时披露所采用的区块长度。

能否对最大回撤进行Bootstrap分析?

可以,但需要注意数据顺序。回撤取决于收益率的排列顺序,因此通过打乱顺序构建的重抽样,会计算重新排列后的历史序列回撤。区块Bootstrap能够保留短期连续走势,更适合处理路径类统计量。

Bootstrap能纠正过拟合吗?

不能。它只能衡量单一收益率序列中的抽样噪声。前视偏差,以及测试多个变体所产生的选择效应,都不在其观测范围内。


这里的每个面板都附有生成该面板的SQL。更换ticker或窗口长度后,您可以在Strasmore终端上自行运行。

#backtesting#bootstrap#statistics#confidence intervals#sharpe ratio