10个月均线择时
价格在10个月均线上方就持有,跌破就换成现金,用简单规则躲开大熊市。
发表后和最近三年的风险调整收益都仍为正
论文发表后,夏普比率 0.71 对比 0.64;年化收益低于基准。
原理
这是最容易上手、也最常被拿来入门的趋势跟踪方法。思路只有一句:价格站在长期均线上方,说明大方向向上,就拿着;跌到均线下方,说明趋势转坏,就先离场。
为什么这么朴素的规则会有用?背后有两个现象。第一,股市的大级别下跌往往不是一天完成的。2000 年科网泡沫破裂、2008 年金融危机,指数都是先慢慢跌破长期均线,再用一两年时间跌掉一半。均线规则抓不住最高点,但能在下跌走到中段时把你带出场。第二,市场情绪有惯性:坏消息出来后,投资者通常要好几个月才完全消化,价格的调整也就拖得很长,这给了慢速规则反应的时间。
Faber 在 2007 年的论文里把这件事讲得特别清楚:他用一百多年的美股数据测试,发现这个规则的长期收益和一直持有差不多,但波动和最大回撤小得多,因为它躲开了几次最惨的熊市。代价也很明确:在来回震荡的市场里,它会反复"跌破就卖、涨回又买",每次都小亏一点;遇到 2020 年那种几周内暴跌又暴涨的行情,它往往卖在低位、买回时已经涨回去了。
所以它的本质不是"多赚钱",而是用偶尔的小亏换掉罕见的大亏。论文发表后它还灵不灵,正是这页回测要回答的问题。
回测结果
净值曲线(月末,起点为 1)
回撤
分段表现(策略 / 买入持有 SPY)
| 全样本 | 发表前 | 发表后 | 最近三年 | |
|---|---|---|---|---|
| 区间 | 1993–2026 | 1993–2007 | 2008–2026 | 2023–2026 |
| 年化收益 | 9.8% 10.8% |
11.6% 10.2% |
8.5% 11.2% |
12.9% 22.0% |
| 年化波动 | 12.8% 18.7% |
13.1% 17.2% |
12.5% 19.7% |
11.9% 15.2% |
| 夏普比率 | 0.80 0.64 |
0.90 0.65 |
0.71 0.64 |
1.08 1.38 |
| 最大回撤 | -24.5% -55.2% |
-19.1% -47.5% |
-24.5% -51.9% |
-10.0% -18.8% |
| 年换手 | 1.4 倍 | 1.1 倍 | 1.7 倍 | 2.0 倍 |
已扣除单边 10bp 交易成本。数据:Yahoo Finance 日线(复权)。发表前后以论文发表年份 2007 为界。
逐年收益
注意事项
- SPY 1993 年才上市,论文用的是 1901 年起的标普指数,本回测只覆盖 ETF 时代。
- 现金部分按 13 周国债利率计息;夏普比率按网站统一口径(无风险利率=0)计算,会略微高估择时策略。
- 信号用月末收盘价,假设当天收盘成交;实际操作可能有一天的偏差。
交易规则
- 标的:SPY(追踪标普 500 的 ETF)。空仓时资金放现金,按 13 周美国国债利率计息。
- 信号日:每个月最后一个交易日收盘后。
- 计算:取最近 10 个月的月末收盘价(复权),求平均,得到 10 个月均线。
- 规则:月末收盘价 高于 均线 → 下个月 100% 持有 SPY;低于 均线 → 下个月 100% 持有现金。
- 一个月只看一次,月中不管涨跌都不操作。
- 交易成本:每次买卖按单边 0.1%(手续费加滑点)扣除。
- 业绩基准:一直持有 SPY 不动。
回测结果解读
回测区间 1993-11-01 至 2026-10-16,结论:仍有效(发表后和最近三年的风险调整收益都仍为正)。
| 区间 | 起止 | 策略年化 | 基准年化 | 策略夏普 | 基准夏普 | 策略最大回撤 | 基准最大回撤 | 年换手 |
|---|---|---|---|---|---|---|---|---|
| 全区间 | 1993-11-01~2026-10-16 | 9.8% | 10.8% | 0.80 | 0.64 | -24.5% | -55.2% | 1.4 |
| 论文发表前 | 1993-11-01~2007-12-31 | 11.6% | 10.2% | 0.90 | 0.65 | -19.1% | -47.5% | 1.1 |
| 论文发表后 | 2008-01-02~2026-10-16 | 8.5% | 11.2% | 0.71 | 0.64 | -24.5% | -51.9% | 1.7 |
| 最近3年 | 2023-10-17~2026-10-16 | 12.9% | 22.0% | 1.08 | 1.38 | -10.0% | -18.8% | 2.0 |
- 这 33 年里,策略约 78% 的月份持有 SPY,其余时间拿现金,一共切换了 46 次。
- 全区间最大回撤 -24.5%,基准是 -55.2%,回撤大约只有基准的一半甚至更少。策略最惨的一段是从 2021年12月 到 2023年02月。
- 论文发表前(2007 年及以前)夏普 0.90,发表后 0.71(同期 SPY 0.64)。发表后的风险调整收益好于基准,回撤大约只有基准的一半甚至更少。
- 论文发表后规则没有被"用坏":它依然在用较小的回撤换来和持有相近的风险调整收益。
- 两次大熊市的表现差别很大:2008 年策略基本持平(+1.4%),SPY 跌了 36.8%;但 2022 年它反复进出,全年 -20.2%,反而比 SPY 的 -18.2% 还差,策略最深的一次回撤也出现在这一段。
最近三年表现
2023-10-17 至 2026-10-16:策略年化 12.9%,SPY 22.0%;夏普 1.08 对 1.38;最大回撤 -10.0% 对 -18.8%。收益不如基准,风险调整后不如基准,回撤大约只有基准的一半甚至更少。
局限与风险
- 震荡市反复打脸:价格在均线附近来回时会频繁进出,每次都小亏,加上成本更明显。
- V 型行情跟不上:急跌急涨(如 2020 年 3 月)时,往往卖在低处、买回时已经涨回。
- 一个月才看一次:月中暴跌不会离场,最大回撤可能比"想象中"深。
- 参数是经验值:10 个月没有特别的理论依据,换成 8 或 12 个月结果会有差异,别过度挑参数。
- 样本只有 ETF 时代:SPY 1993 年上市,比论文的一百年短得多,熊市样本只有几次。
- 税:频繁卖出在很多国家会产生资本利得税,回测没有计入。
历史回测不代表未来收益,仅供学习研究,不构成投资建议。
完整代码
# -*- coding: utf-8 -*-
"""
01 · 10个月均线择时(Faber 2007)
规则:每月最后一个交易日收盘后,看 SPY 收盘价是否高于过去 10 个月月末收盘价的平均值。
高于 → 下一阶段 100% 持有 SPY;低于 → 100% 换成现金(按 13 周国债利率计息)。
运行:python strategy.py (需要能访问 Yahoo Finance)
输出:result.json、README.md、video/(图表和视频脚本)
历史回测不代表未来收益,仅供学习研究,不构成投资建议。
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "_lib"))
sys.path.insert(0, HERE)
import pandas as pd
from data import DataUnavailable, adj_close
from engine import backtest_weights
import report
from runner import finish
META = {
"id": "01", "slug": "faber-ma", "name": "10个月均线择时", "category": "趋势与动量",
"paper": {"authors": "Mebane T. Faber", "year": 2007,
"title": "A Quantitative Approach to Tactical Asset Allocation",
"journal": "Journal of Wealth Management", "url": "https://ssrn.com/abstract=962461"},
"data_level": "A", "universe": "SPY(标普500 ETF),空仓时持有现金(13周国债利率)",
"rebalance": "月度", "benchmark_name": "买入持有 SPY", "cost_bps": 10,
"data_source": "Yahoo Finance 日线(复权)",
}
PARAMS = {"asset": "SPY", "sma_months": 10, "cash": "13周国债(^IRX)"}
CAVEATS = [
"SPY 1993 年才上市,论文用的是 1901 年起的标普指数,本回测只覆盖 ETF 时代。",
"现金部分按 13 周国债利率计息;夏普比率按网站统一口径(无风险利率=0)计算,会略微高估择时策略。",
"信号用月末收盘价,假设当天收盘成交;实际操作可能有一天的偏差。",
]
def signals(px, months):
me = report.month_end_dates(px.index)
m = px.reindex(me)
sma = m.rolling(months).mean()
w = (m > sma).astype(float)
return w[sma.notna()]
def main():
try:
px = adj_close([PARAMS["asset"]], start="1993-01-01")
cash = report.tbill_daily(px.index)
except DataUnavailable as e:
return finish(HERE, META, "10个月均线择时", params=PARAMS,
blocked_reason=f"行情数据下载失败:{e}")
rets = px.pct_change(fill_method=None).iloc[1:]
targets = signals(px[PARAMS["asset"]], PARAMS["sma_months"]).to_frame(PARAMS["asset"])
ret, turn = backtest_weights(targets, rets, META["cost_bps"], cash_returns=cash)
exposure = targets[PARAMS["asset"]].mean()
return finish(HERE, META, "10个月均线择时", ret, rets[PARAMS["asset"]], turn, PARAMS, CAVEATS,
extra_facts={"EXPOSURE": f"{exposure:.0%}",
"SWITCHES": int((targets[PARAMS['asset']].diff().abs() > 0).sum()),
"YEARS": f"{(targets.index[-1] - targets.index[0]).days / 365.25:.0f}"})
if __name__ == "__main__":
main()
历史回测不代表未来收益,仅供学习研究,不构成投资建议。