page contents

我用 Python 跑了 1000 次回测,结果让我惊出一身冷汗

最近读到一篇有趣的实验文章。作者用 Python 构建了一个回测系统,生成了 1000 个完全基于「噪声」的交易策略,结果最好的那个看起来居然像模像样。这个实验揭示了量化研究中一个非常容易被忽视的陷阱 —— 选择偏差(Selection Bias)。

attachments-2026-04-w3S51jv169f164e318e02.png如果有人告诉你他的量化策略夏普比率达到 0.86,年化收益 14.6%,你会心动吗?

最近读到一篇有趣的实验文章。作者用 Python 构建了一个回测系统,生成了 1000 个完全基于「噪声」的交易策略,结果最好的那个看起来居然像模像样。这个实验揭示了量化研究中一个非常容易被忽视的陷阱 —— 选择偏差(Selection Bias)

对于学习 Python、对量化感兴趣的小伙伴来说,这个实验非常值得了解。本文带你一起看看作者是怎么做的,并用 Python 复现核心思想。

实验设计

作者搭建了一个完整的回测流水线,主要参数如下:

  • • 候选策略数量:1000 个,全部基于噪声、弱信号或打乱后的数据
  • • 股票池:标普 500 中的 101 只大盘股
  • • 数据周期:2010 年到 2023 年(来源 Yahoo Finance)
  • • 调仓频率:每周再平衡,等权重持有前 30 只
  • • 交易成本:10 bps 手续费 + 5 bps 滑点
  • • 样本内:2010—2019;样本外:2020—2023

关键设计:这些信号本来就不应该有效。实验目的不是找到好策略,而是看看从一堆噪声里「挑出最好的」会有多么误导人。

让人冷汗直流的结果

1. 中位数夏普 0.67,最高 0.86

1000 个策略里,73 个夏普超过 0.75,但全部都是噪声!更可怕的是,中位数信息比率(相对 SPY)是 -0.37,说明所谓的「收益」基本都是市场 Beta,和策略本身没关系。

2. Best-of-N 陷阱

这是最核心的发现。当你测试的策略数 N 越大,「最佳夏普」就会机械性地升高:

测试数量 N最佳夏普中位数100.7511000.80810000.862

策略本身没变好,你只是搜索得更狠了而已。

3. 样本外性能崩盘

作者拿样本内排名前 20 的策略放到样本外去跑:

  • • 样本内平均夏普:1.004
  • • 样本外平均夏普:0.478
  • • 平均退化 52.4%,无一例外全部下跌

4. 交易成本吃掉了 1/3 的收益

平均夏普被成本拉低 0.303,CAGR 被拉低 6.05 个百分点。

Python 代码案例

下面我们用 Python 简单复现「Best-of-N 陷阱」的核心思想,让大家直观感受一下:

import numpy as np
import
 matplotlib.pyplot as plt

# 设置随机种子,保证结果可复现

np.random.seed(42)

# 模拟 1000 个完全随机的「策略」

# 每个策略生成 10 年的日收益(约 2520 个交易日)

n_strategies = 1000
n_days = 252 * 10

# 生成纯随机收益(均值 0,年化波动 20%)

random_returns = np.random.normal(
    loc=0,
    scale=0.20 / np.sqrt(252),
    size=(n_strategies, n_days)
)

# 计算每个策略的年化夏普比率

def
 annual_sharpe(returns):
    """计算年化夏普比率"""

    mean_daily = returns.mean()
    std_daily = returns.std()
    return
 (mean_daily / std_daily) * np.sqrt(252)

# 计算所有策略的夏普

sharpes = np.array([annual_sharpe(r) for r in random_returns])

# 模拟 Best-of-N 现象

# 从 1000 个策略中随机抽取 N 个,看看「最佳夏普」如何变化

N_levels = [10, 50, 100, 250, 500, 1000]
n_simulations = 2000  # 蒙特卡洛模拟次数

print
("【Best-of-N 选择偏差实验】")
print
("=" * 40)
for
 N in N_levels:
    # 重复 2000 次,每次抽 N 个取最大值

    best_values = []
    for
 _ in range(n_simulations):
        sample = np.random.choice(sharpes, size=N, replace=False)
        best_values.append(sample.max())
    # 输出平均最佳夏普

    print
(f"测试 {N:>4d} 个策略 -> 最佳夏普均值:{np.mean(best_values):.3f}")

# 绘制夏普分布直方图

plt.figure(figsize=(10, 5))
plt.hist(sharpes, bins=50, color='skyblue', edgecolor='black')
plt.axvline(np.median(sharpes), color='red', linestyle='--',
            label=f'中位数:{np.median(sharpes):.2f}')
plt.axvline(np.max(sharpes), color='green', linestyle='--',
            label=f'最大值:{np.max(sharpes):.2f}')
plt.xlabel('年化夏普比率')
plt.ylabel('策略数量')
plt.title('1000 个纯噪声策略的夏普分布')
plt.legend()
plt.tight_layout()
plt.show()

运行后你会发现:测试的策略越多,「看起来最好」的那个就越漂亮,但本质上它们都是噪声

给 Python 量化新手的几点启示

  1. 1. 小心样本内表现:漂亮的回测曲线可能只是过拟合到了历史数据上的噪声。
  2. 2. 关注信息比率(IR),而不只是夏普:长仓策略的夏普大部分来自市场 Beta,IR 才能反映真正的「Alpha」。
  3. 3. 记录每一次尝试:测试了多少个参数、多少个变体,都要老老实实记下来。N 越大,幸存者偏差越严重。
  4. 4. 务必做样本外验证:最好用 Walk-Forward 滚动验证,不要只切一刀。
  5. 5. 永远不要忘记交易成本:实验中成本吃掉了 1/3 的收益,实盘只会更狠。
  6. 6. survivorship bias(幸存者偏差)也要考虑:用当前的标普 500 成分股回测,本身就剔除了那些被踢出去的「失败者」。

总结

这个实验最有价值的不是某个具体策略,而是它用 1000 次实验告诉我们:当你在足够大的搜索空间里寻找「好结果」时,运气一定会给你一个看起来很美的答案

对于刚入门 Python 量化的同学,与其追求一个夏普 1.0+ 的「圣杯策略」,不如先把回测框架的严谨性、样本外验证、交易成本建模这些基本功打扎实。好的研究流程比好的回测结果重要得多

记住一句话:The numbers are real. The alpha isn't.(数字是真的,超额收益是假的。)

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1999 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 1999 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章