Python 量化技术指标

技术指标是对价格与成交量序列的数学变换,把原始行情压缩成几个可比较的数字。

本篇不满足于"调库出数":MACD、RSI、布林带、ATR 四个最常用的指标全部亲手推导,最后用 pandas-ta 验证手写结果的正确性。


技术指标的分类

上百种技术指标归纳起来就四类:

类别 回答的问题 代表指标
趋势类 价格往哪个方向走 MA、MACD、DMI
动量类 涨跌的力度与超买超卖 RSI、KDJ、ROC
波动率类 价格波动的剧烈程度 ATR、布林带、历史波动率
成交量类 资金参与的热度 OBV、成交量均线

本篇四个指标恰好覆盖前三类。动手之前先统一数据:使用茅台 2023-2024 的复权收盘价(复权的重要性见金融市场基础)。

实例

import yfinance as yf

# 统一使用复权收盘价
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)

实例

# 快线 DIF:12 日 EMA 与 26 日 EMA 之差
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 matplotlib.pyplot as plt
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()

执行以上代码,输出的图表如下:

MACD 指标图

注意两年里出现了 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 倍标准差)构成,通道宽度随波动率自动伸缩:

实例

# 中轨:20 日简单移动平均
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

实例

fig, ax = plt.subplots(figsize=(12, 6))
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 候选 计算 覆盖情形
当日波幅 最高价 - 最低价 普通交易日
向上跳空 |最高价 - 昨收| 跳空高开
向下跳空 |最低价 - 昨收| 跳空低开

实例

# ATR 需要最高、最低、收盘价,注意统一使用未复权数据
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 要求列名为小写):

实例

import pandas_ta as 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"导致指标对不上号。指标计算的输入口径必须统一,这个坑比算法本身常见得多。