Python 量化技术指标
技术指标是对价格与成交量序列的数学变换,把原始行情压缩成几个可比较的数字。
本篇不满足于"调库出数":MACD、RSI、布林带、ATR 四个最常用的指标全部亲手推导,最后用 pandas-ta 验证手写结果的正确性。
技术指标的分类
上百种技术指标归纳起来就四类:
| 类别 | 回答的问题 | 代表指标 |
|---|---|---|
| 趋势类 | 价格往哪个方向走 | MA、MACD、DMI |
| 动量类 | 涨跌的力度与超买超卖 | RSI、KDJ、ROC |
| 波动率类 | 价格波动的剧烈程度 | ATR、布林带、历史波动率 |
| 成交量类 | 资金参与的热度 | OBV、成交量均线 |
本篇四个指标恰好覆盖前三类。动手之前先统一数据:使用茅台 2023-2024 的复权收盘价(复权的重要性见金融市场基础)。
实例
# 统一使用复权收盘价
close = yf.download("600519.SS", start="2023-01-01", end="2024-12-31",
progress=False, multi_level_index=False)["Close"]
MACD:趋势的快慢对话
MACD(Moving Average Convergence Divergence,指数平滑异同移动平均线)由三条线组成,计算只用到指数移动平均 ewm:
| 组成 | 计算 | 含义 |
|---|---|---|
| DIF(快线) | EMA(12) - EMA(26) | 短期趋势相对长期趋势的偏离 |
| DEA(慢线) | DIF 的 EMA(9) | DIF 自身的平滑 |
| MACD 柱 | 2 × (DIF - DEA) | 快慢线的背离程度(国内软件习惯乘 2) |
实例
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
dif = ema12 - ema26
# 慢线 DEA:DIF 的 9 日 EMA
dea = dif.ewm(span=9, adjust=False).mean()
# MACD 柱:国内行情软件习惯乘以 2
hist = 2 * (dif - dea)
macd = pd.DataFrame({"DIF": dif, "DEA": dea, "MACD": hist})
print(macd.tail(3).round(2).to_string())
# 统计两年内的金叉死叉次数
cross_up = ((dif > dea) & (dif.shift(1) <= dea)).sum()
cross_dn = ((dif < dea) & (dif.shift(1) >= dea)).sum()
print(f"两年内金叉 {cross_up} 次,死叉 {cross_dn} 次")
输出结果如下所示:
DIF DEA MACD
Date
2024-12-26 4.68 2.71 3.95
2024-12-27 4.65 3.09 3.10
2024-12-30 4.27 3.33 1.88
两年内金叉 29 次,死叉 28 次
ewm 是关键:span 参数对应均线的"周期",adjust=False 保证与行情软件算法一致。
把价格与 MACD 画成上下两个子图:
实例
import numpy as np
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True,
gridspec_kw={"height_ratios": [2, 1]})
ax1.plot(close.index, close.values, color="#1f2933",
linewidth=1.2, label="Close")
ax1.set_title("Maotai (600519.SS) Close and MACD (12, 26, 9)")
ax1.set_ylabel("Price (CNY)")
ax1.legend()
ax1.grid(alpha=0.25)
# MACD 柱:正值绿色、负值红色
bar_colors = np.where(hist >= 0, "#14805e", "#c0563f")
ax2.bar(hist.index, hist.values, color=bar_colors, width=1.0)
ax2.plot(dif.index, dif.values, color="#1f2933", linewidth=1.0, label="DIF")
ax2.plot(dea.index, dea.values, color="#e0a458", linewidth=1.0, label="DEA")
ax2.axhline(0, color="#8a95a1", linewidth=0.8)
ax2.set_ylabel("MACD")
ax2.legend()
ax2.grid(alpha=0.25)
fig.tight_layout()
plt.show()
执行以上代码,输出的图表如下:

注意两年里出现了 29 次金叉——DIF/DEA 的交叉远比均线的金叉频繁,直接拿它当交易信号会产生大量噪音,实盘通常还要叠加趋势过滤。
RSI:超买超卖的温度计
RSI(Relative Strength Index,相对强弱指数)衡量一段时间内上涨力量的占比,取值 0 到 100:
实例
delta = close.diff()
gain = delta.clip(lower=0) # 上涨幅度,下跌日记 0
loss = -delta.clip(upper=0) # 下跌幅度,上涨日记 0
# Wilder 平滑(等价于 alpha=1/14 的 EMA),这是 RSI 的标准算法
avg_gain = gain.ewm(alpha=1 / 14, adjust=False).mean()
avg_loss = loss.ewm(alpha=1 / 14, adjust=False).mean()
rs = avg_gain / avg_loss
rsi = 100 - 100 / (1 + rs)
print("最新 3 日 RSI:")
print(rsi.tail(3).round(2).to_string())
输出结果如下所示:
最新 3 日 RSI: Date 2024-12-26 52.32 2024-12-27 52.63 2024-12-30 51.40
RSI 高于 70 惯称超买、低于 30 惯称超卖,茅台最新 RSI 在 51 附近,处于中性区域。
注意:RSI 的平滑方式有 Wilder 平滑与简单均值两种,不同软件算出的数值会有差异。本篇使用 Wilder 平滑,与主流行情软件一致。
布林带:波动率画出的通道
布林带(Bollinger Bands)由中轨(20 日均线)与上下两轨(中轨 ± 2 倍标准差)构成,通道宽度随波动率自动伸缩:
实例
mid = close.rolling(20).mean()
# 标准差衡量波动
std = close.rolling(20).std()
upper = mid + 2 * std # 上轨
lower = mid - 2 * std # 下轨
boll = pd.DataFrame({"Upper": upper, "Mid": mid, "Lower": lower})
print(boll.tail(3).round(2).to_string())
输出结果如下所示:
Upper Mid Lower
Date
2024-12-26 1461.87 1427.79 1393.71
2024-12-27 1463.02 1429.05 1395.08
2024-12-30 1463.56 1430.15 1396.75
实例
ax.plot(close.index, close.values, color="#1f2933",
linewidth=1.2, label="Close")
ax.plot(mid.index, mid.values, color="#e0a458",
linewidth=1.2, label="Middle Band (MA20)")
ax.plot(upper.index, upper.values, color="#14805e",
linewidth=1.0, linestyle="--", label="Upper Band")
ax.plot(lower.index, lower.values, color="#c0563f",
linewidth=1.0, linestyle="--", label="Lower Band")
ax.fill_between(lower.index, lower.values, upper.values,
color="#14805e", alpha=0.06)
ax.set_title("Maotai: Bollinger Bands (20, 2)")
ax.set_xlabel("Date")
ax.set_ylabel("Price (CNY)")
ax.legend()
ax.grid(alpha=0.25)
plt.show()
执行以上代码,输出的图表如下:

价格触及下轨说明短期偏离均值较大,这正是后面均值回归策略的入场依据之一。
ATR:波动多少钱
ATR(Average True Range,平均真实波幅)把"今天波动有多大"变成一个以元为单位的具体数字,是设置止损距离的经典依据。
它基于真实波幅 TR——以下三者取最大:
| TR 候选 | 计算 | 覆盖情形 |
|---|---|---|
| 当日波幅 | 最高价 - 最低价 | 普通交易日 |
| 向上跳空 | |最高价 - 昨收| | 跳空高开 |
| 向下跳空 | |最低价 - 昨收| | 跳空低开 |
实例
ohlc = yf.download("600519.SS", start="2023-01-01", end="2024-12-31",
auto_adjust=False, progress=False,
multi_level_index=False)
high, low, close_u = ohlc["High"], ohlc["Low"], ohlc["Close"]
# TR:三个候选取最大
tr = pd.concat([
high - low,
(high - close_u.shift()).abs(),
(low - close_u.shift()).abs(),
], axis=1).max(axis=1)
# Wilder 平滑得到 ATR
atr = tr.ewm(alpha=1 / 14, adjust=False).mean()
print("最新 3 日 TR/ATR:")
print(pd.DataFrame({"TR": tr, "ATR14": atr}).tail(3).round(2).to_string())
输出结果如下所示:
最新 3 日 TR/ATR:
TR ATR14
Date
2024-12-26 15.78 28.23
2024-12-27 16.50 27.40
2024-12-30 18.96 26.79
茅台的 ATR 约为 27 元——单日平均真实波动 27 元左右。止损设多远?很多趋势策略采用"2 倍 ATR"这类规则,让止损距离与品种的实际波动匹配。
易错点:ATR 必须用同一口径的最高/最低/收盘价计算。把复权收盘价和未复权最高价混在一起,TR 会算出天大的错值。
用 pandas-ta 验证手写结果
自己写一遍是为了理解,日常使用可以用现成库。pandas-ta 提供了上百个指标的向量化实现:
# 注意:pandas-ta 0.4.x 需要 Python 3.10 及以上 # Python 版本不足可用 uv 一键切换(见虚拟环境一篇):uv venv --python 3.12 pip install pandas-ta
用 pandas-ta 重算同样的指标,与手写结果对比(注意 pandas-ta 要求列名为小写):
实例
macd_ta = ta.macd(close) # 返回 DIF/DEA/柱 三列
rsi_ta = ta.rsi(close, length=14)
bb_ta = ta.bbands(close, length=20, std=2) # 返回上下中轨等五列
print("MACD DIF 手写/pandas_ta:", round(dif.iloc[-1], 4),
"/", round(macd_ta["MACD_12_26_9"].iloc[-1], 4))
print("MACD DEA 手写/pandas_ta:", round(dea.iloc[-1], 4),
"/", round(macd_ta["MACDs_12_26_9"].iloc[-1], 4))
print("RSI14 手写/pandas_ta:", round(rsi.iloc[-1], 4),
"/", round(rsi_ta.iloc[-1], 4))
print("布林上轨 手写/pandas_ta:", round(upper.iloc[-1], 4),
"/", round(bb_ta["BBU_20_2.0_2.0"].iloc[-1], 4))
print("布林下轨 手写/pandas_ta:", round(lower.iloc[-1], 4),
"/", round(bb_ta["BBL_20_2.0_2.0"].iloc[-1], 4))
输出结果如下所示:
MACD DIF 手写/pandas_ta: 4.2672 / 4.2672 MACD DEA 手写/pandas_ta: 3.3281 / 3.3281 RSI14 手写/pandas_ta: 51.4014 / 51.4014 布林上轨 手写/pandas_ta: 1463.5587 / 1463.5587 布林下轨 手写/pandas_ta: 1396.7471 / 1396.7471
五组数值完全一致——手写实现经得起验证。
另一个老牌选择 TA-Lib 功能更强(含形态识别),但依赖 C 库安装麻烦;pandas-ta 纯 Python 实现,个人研究更省心。
教训分享:验证时曾因"复权价喂给 ATR、未复权价喂给 MACD"导致指标对不上号。指标计算的输入口径必须统一,这个坑比算法本身常见得多。
点我分享笔记