
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含300篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
你是否想过,一套简单的技术指标组合,能否在长达 25 年的股市中稳定盈利?
今天要分享的是一篇来自 Medium 的量化交易策略文章。作者使用 Python 对美股 Cigna(股票代码:CI)进行了从 2000 年到 2025 年的回测,仅凭两个技术指标——EMA(指数移动平均线) 和 DI−(负方向指标),构建了一套完整的趋势跟踪交易系统。
这篇文章非常适合正在学习 Python 量化交易的同学,涉及到数据获取、指标计算、信号生成和组合回测等核心环节。下面我们就来拆解这套策略的完整逻辑和代码实现。
这套策略的名称是 CI-EMA Breakout with DI Deceleration Strategy,核心逻辑可以用两句话概括:
策略不做预测,只做反应——这是典型的趋势跟踪系统的核心哲学。
首先需要安装以下 Python 库:
pip install pandas numpy yfinance vectorbt然后下载 Cigna 的历史日线数据:
import pandas as pd
import numpy as np
import yfinance as yf
import vectorbt as vbt
# -------------------------
# 下载股票数据
# -------------------------
symbol = "CI" # 股票代码:Cigna
start_date = "2000-01-01" # 起始日期
end_date = "2026-01-01" # 结束日期
interval = "1d" # 日线级别
# 通过 yfinance 下载数据
df = yf.download(symbol, start=start_date, end=end_date, interval=interval)
# 保存为 CSV 文件(可选)
df.to_csv("CI_clean.csv", index=False)提示:yfinance 是一个免费的 Python 库,可以方便地从 Yahoo Finance 获取股票历史数据。
EMA(Exponential Moving Average,指数移动平均线) 是一种对近期价格赋予更高权重的均线,比简单移动平均线(SMA)更灵敏。
策略规则很简单:收盘价 > 20 日 EMA,则产生买入信号。
EMA_PERIOD = 20 # EMA 周期设为 20 天
def calculate_ema(df, period=EMA_PERIOD):
"""
计算收盘价的指数移动平均线(EMA)。
"""
df = df.copy()
# 使用 pandas 的 ewm 方法计算 EMA
df['EMA'] = df['Close'].ewm(span=period, adjust=False).mean()
return df
def close_above_ema(df, period=EMA_PERIOD):
"""
判断收盘价是否在 EMA 之上。
返回布尔序列:True 表示收盘价高于 EMA。
"""
df = calculate_ema(df, period)
return df['Close'] > df['EMA']生成入场信号:
# -------------------------
# 入场条件
# -------------------------
df["EMA_Close_Above"] = close_above_ema(df)
entry_conditions = [
'EMA_Close_Above', # 收盘价在 EMA 之上
]
# 所有入场条件同时满足时,产生入场信号
df['entry_signal'] = df[entry_conditions].all(axis=1)这个条件充当了一个趋势过滤器:价格在 EMA 之上代表多头动能,低于 EMA 则保持观望。
DI−(Directional Indicator Minus,负方向指标) 用于衡量市场的空头压力强度,是 ADX 指标体系的一部分。
出场逻辑是:DI− 之前在上升(空头压力增强),现在开始下降(空头压力减弱),说明下跌动能正在衰退,此时卖出锁定利润。
DI_PERIOD = 14 # DI 计算周期
DI_SHIFT_1 = 5 # 近期偏移量
DI_SHIFT_2 = 10 # 远期偏移量
def calculate_di(df, period=DI_PERIOD):
"""
计算 DI+(正方向指标)和 DI-(负方向指标)。
"""
df = df.copy()
# 计算真实波幅(True Range)
df['H-L'] = df['High'] - df['Low']
df['H-PC'] = abs(df['High'] - df['Close'].shift(1))
df['L-PC'] = abs(df['Low'] - df['Close'].shift(1))
df['TR'] = df[['H-L', 'H-PC', 'L-PC']].max(axis=1)
# 计算 +DM(正方向运动)和 -DM(负方向运动)
df['+DM'] = np.where(
(df['High'] - df['High'].shift(1)) > (df['Low'].shift(1) - df['Low']),
np.maximum(df['High'] - df['High'].shift(1), 0),
0
)
df['-DM'] = np.where(
(df['Low'].shift(1) - df['Low']) > (df['High'] - df['High'].shift(1)),
np.maximum(df['Low'].shift(1) - df['Low'], 0),
0
)
# Wilder 平滑处理
df['TR_smooth'] = df['TR'].rolling(period).sum()
df['+DM_smooth'] = df['+DM'].rolling(period).sum()
df['-DM_smooth'] = df['-DM'].rolling(period).sum()
# 计算 DI+ 和 DI-
df['DI+'] = 100 * (df['+DM_smooth'] / df['TR_smooth'])
df['DI-'] = 100 * (df['-DM_smooth'] / df['TR_smooth'])
return df
def di_minus_change_direction_downward(df, shift_1=DI_SHIFT_1, shift_2=DI_SHIFT_2):
"""
判断 DI- 是否从上升趋势转为下降趋势。
即:DI- 在 shift_2 到 shift_1 期间是上升的,但在 shift_1 到当前是下降的。
"""
df = calculate_di(df)
return (df['DI-'].shift(shift_1) < df['DI-'].shift(shift_2)) & \
(df['DI-'] < df['DI-'].shift(shift_1))生成出场信号:
# -------------------------
# 出场条件
# -------------------------
df["DI-_Change_Direction_Downward"] = di_minus_change_direction_downward(df)
exit_conditions = [
'DI-_Change_Direction_Downward', # DI- 方向从上升转为下降
]
# 所有出场条件同时满足时,产生出场信号
df['exit_signal'] = df[exit_conditions].all(axis=1)这种动态出场方式不依赖固定的止盈止损点,而是根据市场动能的变化自适应退出。
使用 vectorbt 库进行组合回测,模拟真实的交易环境:
# -------------------------
# 回测设置
# -------------------------
# 信号延迟一天执行(模拟真实交易:今天产生信号,明天开盘执行)
shift_entries = df['entry_signal'].shift(1).astype(bool).fillna(False).to_numpy()
shift_exits = df['exit_signal'].shift(1).astype(bool).fillna(False).to_numpy()
# 构建投资组合
pf = vbt.Portfolio.from_signals(
close=df['Open'], # 以开盘价成交
entries=shift_entries, # 入场信号
exits=shift_exits, # 出场信号
init_cash=100_000, # 初始资金 10 万美元
fees=0.001, # 手续费 0.1%
slippage=0.002, # 滑点 0.2%
freq='1d' # 日线频率
)
# 输出回测统计信息
print(pf.stats())
# 绘制资金曲线图
pf.plot().show()同时,作者还做了一个买入持有(Buy & Hold) 的对比基准:
# 买入持有策略的回测(作为对比基准)
df_holding = df['Open']
pf_holding = vbt.Portfolio.from_holding(
df_holding,
init_cash=100_000,
fees=0.001,
slippage=0.002
)
print(pf_holding.stats())以下是该策略在 25 年回测期间的核心表现数据:
| 1406% | ||
| 50% | 84% | |
几个关键点值得注意:
关于最大回撤:买入持有的最大回撤高达 84%(经历了 2000 年互联网泡沫和 2008 年金融危机),而策略通过在下跌趋势中及时退出,将最大回撤控制在了 50%。虽然仍然不低,但相比被动持有已经大幅改善。
关于 39% 的胜率:这看起来很低,但这正是趋势跟踪策略的典型特征——不需要高胜率,而是靠"让利润奔跑、快速截断亏损"来实现盈利。1.56 的盈亏比意味着总利润是总亏损的 1.56 倍,这对于趋势系统来说是健康的表现。
优势方面:该策略在长周期回测中展现了比买入持有更好的资金保护能力,能够在主要上升趋势中获取收益,并在弱势行情中保持空仓。策略逻辑清晰,完全基于规则执行,排除了情绪干扰。
局限方面:50% 的最大回撤依然具有很大的心理压力;39% 的胜率要求交易者有足够的纪律和耐心。此外,作者也坦承,仅仅在历史数据上回测是不够的,还需要进行更严格的稳健性检验(如 Walk-Forward 优化)来验证策略在未来市场中的可靠性。
这篇文章展示了一套基于 EMA 突破入场 + DI− 方向反转出场 的趋势跟踪策略,使用 Python 的 yfinance 和 vectorbt 库完成了从数据获取到回测分析的全流程。
对于 Python 量化交易的学习者来说,这个案例有几个重要的学习价值:
正如文章作者所说:一致性胜过预测(Consistency beats prediction)。 在量化交易中,纪律和系统化思维远比猜测市场方向重要。
免责声明:本文仅用于 Python 学习和技术交流,不构成任何投资建议。投资有风险,入市需谨慎。任何交易系统在实盘使用前,都应进行充分的研究和测试。
加入专注于财经数据与量化投研的知识星球【数据科学实战】,获取本文完整研究解析、代码实现细节。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐