Py学习  »  Python

用 Python 回测 S&P 500 的简单移动平均线策略,结果出乎意料!

数据科学实战 • 5 月前 • 302 次点击  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

引言

你是否好奇过,交易员是如何在趋势完全展开之前就捕捉到它的?移动平均线(Moving Average,简称 MA)是金融领域中最经典的技术指标之一,它通过平滑价格数据来帮助我们识别市场趋势、过滤噪声。

本文将带你使用 Python 对 S&P 500 指数进行多窗口简单移动平均线(SMA)策略的回测,从风险调整收益、波动率和最大回撤等维度全面评估不同 SMA 窗口的表现。无论你是量化交易的初学者,还是正在学习 Python 数据分析的开发者,这篇文章都能帮你建立对 SMA 回测的完整认知。


什么是简单移动平均线(SMA)?

SMA 是一种有限脉冲响应(FIR)平滑滤波器,本质上是一个低通滤波器。它通过对过去 N 个时间点的价格取平均值,让低频成分(如趋势)通过,同时削弱高频成分(如短期噪声波动)。

SMA 的计算公式为:

SMA(t) = [P(t) + P(t-1) + … + P(t-N+1)] / N

其中 P(t) 为当前价格,N 为窗口长度。这个公式是**因果(causal)**的,即只依赖已知的历史数据,不会引入未来信息。


两个核心问题

1. 前瞻偏差(Lookahead Bias)

前瞻偏差是指在回测中使用了未来的数据。例如,如果移动平均线的计算包含了未来的价格,那么策略在模拟中看起来很赚钱,但在实盘中会彻底失效。因此,我们必须确保 SMA 只使用当前和过去的数据,并且在生成交易信号后延迟一天执行

2. 滞后-长度困境(Lag-Length Dilemma)

SMA 的滞后约为 (N-1)/2 个周期。选择窗口长度 N 时存在一个根本性的权衡:

  • • 窗口短:响应快、滞后低,但噪声大,容易产生虚假信号
  • • 窗口长:更平滑、噪声低,但信号延迟,导致入场和出场偏晚

你无法同时获得低噪声和低滞后,这就是滞后-长度困境的本质。


实战:多窗口 SMA 回测

下面我们用 Python 对 S&P 500 近 10 年的日线数据进行多窗口 SMA 回测。

第一步:获取数据

import requests
import
 pandas as pd
import
 numpy as  np
import
 matplotlib.pyplot as plt
from
 datetime import datetime, timedelta

# ---------------------------

# 配置参数

# ---------------------------

API_KEY = "YOUR API KEY"  # 替换为你自己的 EODHD API 密钥
SYMBOL = 'GSPC.INDX'     # S&P 500 指数代码

# ---------------------------

# 下载数据(近 10 年日线数据)

# ---------------------------

end_date = datetime.today()
start_date = end_date - timedelta(days=365 * 10)

url = f"https://eodhd.com/api/eod/{SYMBOL}"
params = {
    "from"
: start_date.strftime("%Y-%m-%d"),
    "to"
: end_date.strftime("%Y-%m-%d"),
    "period"
: "d",        # 日线级别
    "fmt"
: "json",        # 返回 JSON 格式
    "api_token"
: API_KEY
}

data = requests.get(url, params=params).json()
df = pd.DataFrame(data)  # 约 2513 条记录
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)
df.sort_values('date', inplace=True)
df.reset_index(drop=True, inplace=True)  # 重置索引,便于后续处理

# 提取收盘价

prices = df['close']

第二步:计算日收益率并定义回测窗口

# 计算日收益率,首日用 0 填充
returns = prices.pct_change().fillna(0)

# 定义多个 SMA 窗口长度(短期、中期、长期)

windows = [10, 20, 50, 100, 200]

# 初始化存储字典

results = {}        # 绩效指标
equity_curves = {}  # 累计收益曲线
drawdowns = {}      # 回撤序列
volatility = {}     # 波动率序列

第三步:执行回测核心逻辑

for w in windows:
    # 计算 w 日简单移动平均线

    sma = prices.rolling(w).mean()

    # 生成趋势信号:价格 > SMA 时为 1(做多),否则为 0(空仓)

    signal = (prices > sma).astype(int)

    # 将信号延后一天执行,避免前瞻偏差

    position = signal.shift(1).fillna(0)

    # 策略收益 = 仓位 × 市场收益(仅在持仓时获取收益)

    strat_ret = position * returns

    # 构建权益曲线(模拟 1 美元的增长路径)

    equity = (1 + strat_ret).cumprod()

    # 计算回撤

    peak = equity.cummax()          # 历史最高净值
    dd = (equity - peak) / peak     # 回撤百分比

    # 计算滚动波动率(年化)

    vol = strat_ret.rolling(w).std() * np.sqrt(252)

    # 存储结果

    equity_curves[w] = equity
    drawdowns[w] = dd
    volatility[w] = vol

    # 计算年化收益率

    ann_return = equity.iloc[-1] ** (252 / len(equity)) - 1
    # 计算年化波动率

    ann_vol = strat_ret.std() * np.sqrt(252)
    # 计算夏普比率(收益/风险)

    sharpe = ann_return / ann_vol if ann_vol != 0 else 0
    # 计算最大回撤

    max_dd = dd.min()

    results[w] = {
        "Total Return"
: equity.iloc[-1] - 1,   # 总收益率
        "Annual Return"
: ann_return,            # 年化收益率
        "Volatility"
: ann_vol,                  # 年化波动率
        "Sharpe"
: sharpe,                       # 夏普比率
        "Max Drawdown"
: max_dd                  # 最大回撤
    }

# 汇总为 DataFrame

perf = pd.DataFrame(results).T

第四步:可视化累计收益曲线

plt.figure(figsize=(12, 6))
for
 w in windows:
    plt.plot(equity_curves[w], label=f"SMA {w}")
plt.title("Cumulative Returns of Multi-SMA Strategies")
plt.ylabel("Equity")
plt.xlabel("Time Index")
plt.legend()
plt.grid(True)
plt.show()

第五步:查看绩效汇总表

print(perf.round(3))

输出结果如下:

窗口
总收益率
年化收益率
波动率
夏普比率
最大回撤
10
0.862
0.064
0.107
0.603
-0.210
20
1.055
0.075
0.104
0.720
-0.181
50
0.967
0.070
0.105
0.669
-0.213
100
1.110
0.078
0.110
0.706
-0.193
200
1.076
0.076
0.114
0.664
-0.197

结果解读

从回测数据中可以得出以下关键发现:

夏普比率:所有窗口的夏普比率在 0.60 至 0.72 之间,属于可接受但并不出色的水平。其中 SMA 20 的夏普比率最高(0.720),表明中短期窗口在风险调整收益方面略有优势。

年化收益率:各窗口的年化收益率在 6.4% 至 7.8% 之间,与 S&P 500 长期平均 7%–10% 的回报基本一致,但并未体现出显著的超额收益。SMA 100 以 7.8% 的年化收益领先。

波动率:波动率在 10.4% 至 11.4% 之间,处于市场通常 10%–20% 的正常范围内,说明各窗口的风险控制都比较稳健。

最大回撤:最大回撤在 -18.1% 到 -21.3% 之间。SMA 20 的最大回撤最小(-18.1%),而 SMA 50 最大(-21.3%)。总体来看,大部分策略的回撤都在可接受范围内。


总结

通过对 S&P 500 指数近 10 年数据的多窗口 SMA 回测,我们可以得出以下结论:

中期 SMA(如 20 日和 100 日)在收益与风险之间提供了最佳平衡。 SMA 20 拥有最高的夏普比率和最小的最大回撤,SMA 100 拥有最高的总收益率和年化收益率。

然而,所有 SMA 策略都未能实现显著的风险调整超额收益,这正是 SMA 的固有局限——短窗口噪声太大,长窗口滞后太多,你无法两全。

如果你希望进一步优化,可以考虑探索其他类型的移动平均线,例如指数移动平均线(EMA)、加权移动平均线(WMA)、双重指数移动平均线(DEMA)等,它们在减少滞后和噪声方面可能有更好的表现。

免责声明:本文仅用于教育和学习目的,不构成任何投资建议。投资决策请咨询专业人士。


参考文章

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

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

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

  1. 1. 双典系统赋能:获赠《财经数据宝典》与《量化投研宝典》完整文档,凝练多年实战经验,构建系统化知识框架;
  2. 2. 量化因子日更教程(2026重磅新增):每日更新「量化因子专题教程」,配套完整可运行代码与实战案例,深度拆解因子构建、回测与优化全流程;
  3. 3. 量化文章专题教程库:300+篇星球独有高质量教程式文章,系统覆盖策略开发、因子研究、风险管理等核心领域,内容基本每日更新,并配套精选学习资料与实战参考;
  4. 4. PyBroker实战课程:赠送《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 实现指南

好书推荐


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/194615