当前位置:首页>python>仅用 2 个指标,跑赢买入持有:用 Python 复现一个真实有效的量化策略

仅用 2 个指标,跑赢买入持有:用 Python 复现一个真实有效的量化策略

  • 2026-10-11 08:26:43
仅用 2 个指标,跑赢买入持有:用 Python 复现一个真实有效的量化策略

用 Python 揭秘均值回归策略:你的收益从何而来?

2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 350 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!

引言

很多人学 Python 都想做量化交易,但一打开教程就被几十个指标、上百行参数劝退。其实,真正能稳定赚钱的策略,往往只用很少的逻辑。

最近看到一个非常有意思的案例:仅用 2 个指标,对 Zoetis(ZTS,辉瑞旗下分拆出来的动物保健龙头)做长线择时,从 2017 年到 2026 年,把 10 万美元变成了 30.96 万美元,总收益 209.60%,而同期买入持有只有 143.02%。更难得的是,它经过了 Walk-Forward Optimization(滚动前向优化)验证,避免了「回测好看、实盘翻车」的常见陷阱。

今天我们就用 Python 把这个策略的核心逻辑拆开讲清楚,并附上可直接运行的代码。


一、策略的核心思想

整个系统只用到 2 个指标:

  1. 1. Kaufman 效率比率(Efficiency Ratio,ER):衡量价格走势的「方向性」。
  2. 2. 历史波动率(Historical Volatility,HV):衡量价格的年化波动水平。

1. Kaufman 效率比率(ER)

ER 的计算非常简单:

  • • 分子:N 天内价格的净变化(绝对值)。
  • • 分母:N 天内每日价格变化的绝对值之和。
  • • 取值范围:0 到 1,越接近 1 表示趋势越强,越接近 0 表示越震荡。

反常识的用法:大多数人用 ER 作为趋势过滤器(高时做多,低时观望)。但本策略相反,只在 ER 处于 0.4~0.6 的中间带时入场——这是市场从震荡转向趋势的过渡区,恰恰是方向性突破的酝酿期。

2. 历史波动率(HV)

HV 即对数收益率的年化滚动标准差,用来判断市场状态。当 HV 从高位向下穿越 0.30 时,意味着波动正在压缩,市场结构可能发生切换,触发离场。

一句话总结策略逻辑:

  • • 入场:ER ∈ [0.4, 0.6]
  • • 离场:HV 由上向下穿越 0.30

二、用 Python 实现两个指标

下面用 pandas 和 numpy 实现 ER 与 HV 的计算。

import numpy as np
import
 pandas as pd
import
 yfinance as yf


def
 kaufman_efficiency_ratio(close: pd.Series, period: int = 16) -> pd.Series:
    """
    计算 Kaufman 效率比率(ER)

    参数:
        close: 收盘价序列
        period: 计算周期,默认 16 天
    返回:
        ER 序列,取值范围 [0, 1]
    """

    # 净价格变化(绝对值)

    net_change = (close - close.shift(period)).abs()
    # 累计每日波动绝对值之和

    daily_change = close.diff().abs()
    total_change = daily_change.rolling(window=period).sum()
    # 防止分母为 0

    er = net_change / total_change.replace(0, np.nan)
    return
 er


def
 historical_volatility(close: pd.Series, period: int = 16) -> pd.Series:
    """
    计算年化历史波动率(HV)

    参数:
        close: 收盘价序列
        period: 计算周期,默认 16 天
    返回:
        年化波动率序列
    """

    # 对数收益率

    log_ret = np.log(close / close.shift(1))
    # 滚动标准差并年化(一年约 252 个交易日)

    hv = log_ret.rolling(window=period).std() * np.sqrt(252)
    return
 hv


# 下载 Zoetis 历史数据

data = yf.download("ZTS", start="2017-01-01", end="2026-04-30", auto_adjust=True)
close = data["Close"].squeeze()  # 转为一维 Series

# 计算两个指标

data["ER"] = kaufman_efficiency_ratio(close, period=16)
data["HV"] = historical_volatility(close, period=16)

print
(data[["Close", "ER", "HV"]].tail())

三、生成交易信号并回测

接下来根据策略规则生成持仓信号,并计算策略收益。

def generate_signals(df: pd.DataFrame,
                     er_low: float = 0.4,
                     er_high: float = 0.6,
                     hv_level: float = 0.3
) -> pd.DataFrame:
    """
    根据 ER 和 HV 生成持仓信号

    入场条件:ER 落在 [er_low, er_high] 区间
    离场条件:HV 由上向下穿越 hv_level
    """

    df = df.copy()
    # 入场信号:ER 处于中间带

    entry = (df["ER"] >= er_low) & (df["ER"] <= er_high)
    # 离场信号:HV 上一日 ≥ 阈值,今日 < 阈值

    exit_ = (df["HV"].shift(1) >= hv_level) & (df["HV"] < hv_level)

    # 按规则生成持仓状态(1 = 持有, 0 = 空仓)

    position = np.zeros(len(df))
    holding = 0
    for
 i in range(len(df)):
        if
 holding == 0 and entry.iloc[i]:
            holding = 1
        elif
 holding == 1 and exit_.iloc[i]:
            holding = 0
        position[i] = holding

    df["position"] = position
    return
 df


def
 backtest(df: pd.DataFrame) -> pd.DataFrame:
    """
    简单回测:基于持仓信号计算策略累计收益
    """

    df = df.copy()
    # 每日收益率

    df["ret"] = df["Close"].pct_change().fillna(0)
    # 策略收益 = 上一期持仓 × 当期收益(避免未来函数)

    df["strategy_ret"] = df["position"].shift(1).fillna(0) * df["ret"]
    # 累计净值

    df["strategy_nav"] = (1 + df["strategy_ret"]).cumprod()
    df["buy_hold_nav"] = (1 + df["ret"]).cumprod()
    return
 df


# 使用 2026 年最优参数(来自 Walk-Forward Optimization)

result = generate_signals(data, er_low=0.4, er_high=0.6, hv_level=0.3)
result = backtest(result)

# 打印最终业绩

final_strategy = result["strategy_nav"].iloc[-1]
final_buyhold = result["buy_hold_nav"].iloc[-1]
print
(f"策略累计收益:{(final_strategy - 1) * 100:.2f}%")
print
(f"买入持有累计收益:{(final_buyhold - 1) * 100:.2f}%")

四、为什么必须用 Walk-Forward Optimization

很多回测「漂亮的不真实」,问题就在于:用全部历史数据找到最优参数,再回到这段数据上验证——这是循环论证。

WFO 的做法是:

  1. 1. 用 4 年数据训练(找最优参数)。
  2. 2. 把最优参数用到下 1 年的「未见数据」上做测试。
  3. 3. 训练窗口向前滚动一年,重复执行。

这样一来,每一笔交易都是在「优化器从未见过的数据」上完成的。原文做了 10 个滚动窗口,10 段样本外测试,结果显示:

  • • 总收益:209.60%(同期买入持有 143.02%)
  • • 胜率:68.97%
  • • 盈亏比:2.08
  • • 最大回撤:37.54%(买入持有 51.84%)
  • • Sharpe 比率:0.79
  • • 交易次数:29 笔
  • • 平均持仓天数:约 67 天

更关键的是,10 个窗口里 ER 周期 16 出现了 6 次,HV 阈值 0.30 出现了 8 次——参数在不同训练区间里高度稳定,说明策略捕捉到的是真实信号,而不是过拟合噪声。


五、用 Monte Carlo 检验稳健性

作者还跑了 1000 次 Block Bootstrap(分块自助法) 蒙特卡洛模拟(每 5 天为一个块,保留收益的自相关结构):

  • • 中位数总收益:206.75%(与实测 209.60% 几乎一致)
  • • 5% 分位数:−2.46%(接近不亏不赚)
  • • 95% 分位数:795.53%
  • • 中位数最大回撤:−37.73%
  • • 5%/95% 分位 Sharpe:0.10 ~ 1.25

实测结果落在分布中位数附近,说明历史成绩不是「运气好」。下面是一个简化的 Block Bootstrap 实现示例:

def block_bootstrap(returns: np.ndarray,
                    block_size: int = 5,
                    n_simulations: int = 1000,
                    horizon: int = None
) -> np.ndarray:
    """
    分块自助法蒙特卡洛模拟

    参数:
        returns: 历史日收益率数组
        block_size: 每个块的长度(保留自相关)
        n_simulations: 模拟次数
        horizon: 模拟序列长度,默认与原始相同
    返回:
        每次模拟的累计总收益数组
    """

    if
 horizon is None:
        horizon = len(returns)
    n_blocks = horizon // block_size + 1
    final_returns = np.zeros(n_simulations)

    for
 sim in range(n_simulations):
        # 随机选择若干起点,拼接出一条新路径

        starts = np.random.randint(0, len(returns) - block_size, size=n_blocks)
        sampled = np.concatenate([returns[s:s + block_size] for s in starts])
        sampled = sampled[:horizon]
        # 计算累计收益

        final_returns[sim] = (1 + sampled).prod() - 1

    return
 final_returns


# 用前面策略收益跑一次模拟

strat_ret = result["strategy_ret"].dropna().values
mc_results = block_bootstrap(strat_ret, block_size=5, n_simulations=1000)

print
(f"中位数收益:{np.median(mc_results) * 100:.2f}%")
print
(f"5% 分位收益:{np.percentile(mc_results, 5) * 100:.2f}%")
print
(f"95% 分位收益:{np.percentile(mc_results, 95) * 100:.2f}%")

六、这个策略给 Python 学习者的启发

抛开收益数字,这个案例真正值得学习的地方有 3 点:

  1. 1. 简单胜过复杂:只用 2 个指标,5 个参数,比上百个因子的黑箱模型更容易理解和调试。
  2. 2. 验证比优化更重要:Walk-Forward 加 Monte Carlo 是检验策略「真假」的两把利刃,比堆砌指标更有价值。
  3. 3. 代码要可复现:作者的整个流水线都基于 pandas、numpy、yfinance、vectorbt、matplotlib——全都是开源库,普通 Python 学习者也能上手。

风险提示也要说清楚:29 笔交易样本不算大,最大回撤 37% 实战中很难扛住,且策略只在 ZTS 单一标的上验证,迁移到其他股票需重新做 WFO。


总结

「这个简单的 2 指标策略跑赢了 Zoetis 的买入持有」并不是魔法,而是把 ER 用作市场状态识别、把 HV 用作离场触发器的一个组合。它真正的价值不在于多赚了几十个百分点,而在于:

  • • 思路简单到可以用 50 行 Python 写完;
  • • 验证方法严谨到能让回测数字「站得住脚」;
  • • 提醒我们:真正的边际,往往藏在「克制」与「严谨」里。

如果你正在学 Python 量化,不妨把这个例子作为一个小项目跑一遍,自己动手实现 ER、HV、WFO 和 Monte Carlo——相比于又看一遍教程,这才是更有效的进阶方式。


参考文章

加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取本文完整研究解析、代码实现细节。

财经数据与量化投研知识社区

2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:

  1. 1. 双典系统赋能:获赠《财经数据宝典》与《量化投研宝典》完整文档,凝练多年实战经验,构建系统化知识框架;
  2. 2. 量化因子日更教程(2026重磅新增):每日更新「量化因子专题教程」,配套完整可运行代码与实战案例,深度拆解因子构建、回测与优化全流程;
  3. 3. 量化文章专题教程库:300+篇星球独有高质量教程式文章,系统覆盖策略开发、因子研究、风险管理等核心领域,内容基本每日更新,并配套精选学习资料与实战参考;
  4. 4. 量化投研实战课程:赠送《AKQuant-入门及实战》《PyBroker-入门及实战》视频课程,手把手教学,快速掌握量化策略开发技能;
  5. 5. 财经数据支持:定期更新国内外财经数据,为策略研发提供精准、可靠的数据基础;
  6. 6. 顶尖学者与行业专家分享:年度邀请学术界博士与业界资深专家开展前沿论文精讲与实战案例分享,不少于4场,直击研究前沿与产业实践;
    专家直连答疑:与核心开发者及领域专家实时互动,高效解决投研实战难题;
  7. 7. 专业社群与专属福利:加入高质量交流社群,获取课程折扣及更多独家资源。

星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!

好文推荐

1. 用 Python 打造股票预测系统:Transformer 模型教程(一)

2. 用 Python 打造股票预测系统:Transformer 模型教程(二)

3. 用 Python 打造股票预测系统:Transformer 模型教程(三)

4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)

5. 揭秘隐马尔可夫模型:因子投资的制胜武器

6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用

7. 金融 AI 助手:FinGPT 让你轻松掌握市场分析

8. 量化交易秘籍:为什么专业交易员都在用对数收益率?

9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解

10. 掌握金融波动率模型:完整 Python 实现指南

好书推荐

最新文章

随机文章