到这里,我们已经手工写了十来个技术指标:从第 9 篇的收益率与波动率,到 SMA/EMA、MACD、RSI、布林带、ATR/KDJ,再到上一篇的 OBV,每一个公式都是用 pandas 一行行滚出来的。手写的过程让我们真正理解了「指数平滑」「Wilder 平滑」这些词的分量——这是很多人写了好几年策略都没搞清楚的细节。
上一篇我们走进了量价分析,用成交量、换手率和 OBV 给价格信号找旁证,至此阶段二的指标全家福已经集齐。但不知道你有没有留意到一件有点累人的事:每换一个指标,就要重写一遍滚动窗口、重调一遍参数、再踩一遍预热期的 NaN。真实工作里没人这样干活——大家都是把指标计算交给指标库(Indicator Library),也就是把常用指标写好、测好、打包好的现成代码。本篇作为阶段二的收官,就来认识最主流的两位「指标流水线工人」:TA-Lib 与 pandas-ta。学完之后,你既能用它们一分钟算出前几篇写过的所有指标,还能反向用它们验证自己的手写代码——这种「两套实现互相核对」的习惯,会一直陪你走到实盘。
本篇目标
- 能说出 TA-Lib 与 pandas-ta 的定位差异,并根据自己的系统选对安装方案(含 Windows 装不上时的三条退路)。
- 能用指标库一次性算出 SMA、EMA、MACD、RSI、ATR 五个常用指标。
- 能把库算出的指标与自己手写的版本逐根对齐,用「跳过预热期 + 容差比较」完成一致性核对。
- 能给出「什么场合手写、什么场合用库」的明确选择标准,并说明理由。
核心概念
为什么需要指标库
手写一遍指标的价值,阶段二你已经体会到了:不动手推一遍 EMA 的递推,就永远不知道「初始值怎么给」这个问题的存在。但写策略是长期工程,每次都现场手推,有三个现实问题。
第一是正确性。 像 Wilder 平滑这类细节,十个人手写九个会出错——RSI 里亏损均值为零时分母怎么办、ATR 首根真实波幅怎么处理,都是容易埋雷的地方。指标库的实现被全球用户用了几十年,坑早就踩过了。
第二是性能。 TA-Lib 的核心是 C 语言实现,速度比等价的 pandas 写法快一个数量级。回测要反复算指标,速度就是迭代次数。
第三是覆盖面。 TA-Lib 内置 150 多个指标函数,pandas-ta 内置 130 多个。我们不可能、也没必要全部手写。
一句话:手写是为了懂原理,指标库是为了干活,两者是接力棒,不是敌人。
TA-Lib:功能最强,安装最难
TA-Lib 本体是上世纪 90 年代用 C 语言写的开源技术分析库,几乎是金融行业算技术指标的事实标准,Python 只是它的「外套」。正因为内核是 C,安装时本机得有编译好的 C 库——这就是它著名的安装门槛。
Windows 上按下面三条路依次尝试,绝大多数人能走通:
- 先直接
uv pip install ta-lib。TA-Lib 从 0.5 版本起在 PyPI 提供了 Windows 预编译轮子,新版大概率一把装好。注意:安装时包名叫ta-lib,写代码时import talib。 - 轮子装不上就先补 C 库再重试:Windows 可从 TA-Lib 官方 GitHub Releases 下载预编译库并按 README 配好路径,然后重新执行
uv pip install ta-lib;嫌麻烦可直接跳到第 3 条。 - 还不行就放弃 TA-Lib,改用下一节的 pandas-ta。本篇代码做了自动探测,两种库都能跑;两个库都没装,也能退回纯手写版对照,保证代码一定能运行。
Linux 和 macOS 上 C 库获取容易,uv pip install ta-lib 一般直接成功。
五个常用函数先混个脸熟,右边是我们前几篇的手写对应:
| 指标 | TA-Lib 调用 | 手写对应 |
|---|---|---|
| SMA | talib.SMA(close, timeperiod=20) |
close.rolling(20).mean() |
| EMA | talib.EMA(close, timeperiod=20) |
close.ewm(span=20, adjust=False).mean() |
| MACD | talib.MACD(close),默认 12/26/9 |
快慢 EMA 相减 |
| RSI | talib.RSI(close, timeperiod=14) |
涨跌幅的 Wilder 平滑 |
| ATR | talib.ATR(high, low, close, timeperiod=14) |
真实波幅的 Wilder 平滑 |
注意:TA-Lib 的函数只认 float64 的 NumPy 数组,传 Series 之前先 to_numpy(dtype="float64")。
pandas-ta:轻量好装的替代
pandas-ta 是纯 Python(基于 pandas 和 NumPy)实现的指标库,uv pip install pandas-ta 一步到位,没有 C 库的安装烦恼。调用风格更贴近 pandas 习惯:
import pandas_ta as ta
sma20 = ta.sma(close, length=20) # 返回 Series,名字叫 SMA_20
rsi14 = ta.rsi(close, length=14) # 返回 Series,名字叫 RSI_14
两个使用细节:一是返回结果的列名会带上参数后缀(比如 SMA_20、MACDh_12_26_9),取列时要留意;二是它比 TA-Lib 慢一些,但教学和日常研究完全够用。
划重点:装环境的原则是「先 uv pip 装轮子、装不上就换库」,别在 TA-Lib 的安装报错上死磕一晚上。指标库只是工具,阶段二学的指标原理才是跟着你走的资产。
库指标与手写指标怎么选
| 维度 | 手写(pandas) | TA-Lib | pandas-ta |
|---|---|---|---|
| 安装门槛 | 无 | 高(要 C 库) | 无 |
| 计算速度 | 中 | 最快(C 内核) | 中 |
| 指标覆盖 | 写多少有多少 | 150+ | 130+ |
| 正确性 | 自己负责 | 久经检验 | 久经检验 |
| 学习价值 | 最高 | 低 | 低 |
结论很明确:学习期至少手写一遍,写策略时用库,交付前用另一个实现交叉核对。本篇接下来就演示「交叉核对」怎么做——它会成为你以后排查指标 bug 的标准动作。
完整代码
依赖安装:uv pip install akshare pandas matplotlib;指标库二选一:uv pip install ta-lib 或 uv pip install pandas-ta;两个都没装也能跑,代码会自动退回手写版。
第一个代码块:取数(和系列其他篇保持一致,akshare 失败自动降级为本地模拟数据):
import numpy as np
import pandas as pd
def load_data():
"""优先用 akshare 拉真实行情,失败则退回本地模拟数据,保证代码一定能跑。"""
try:
import akshare as ak
raw = ak.stock_zh_a_hist( # A 股日线行情接口
symbol="000001", period="daily",
start_date="20200101", end_date="20241231", adjust="qfq",
)
raw = raw.rename(columns={
"日期": "date", "开盘": "open", "收盘": "close",
"最高": "high", "最低": "low", "成交量": "volume",
})
df = raw[["date", "open", "high", "low", "close", "volume"]].copy()
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")
print(f"已加载 akshare 真实数据:平安银行,共 {len(df)} 个交易日")
except Exception as e: # 网络不通、接口变动时走这里
print(f"akshare 不可用({e}),改用本地模拟数据")
rng = np.random.default_rng(42) # 固定种子,结果可复现
dates = pd.bdate_range("2020-01-01", "2024-12-31")
n = len(dates)
ret = rng.normal(0.0002, 0.018, n) # 日收益:微小漂移 + 波动
close = 10.0 * np.cumprod(1 + ret)
open_ = close * (1 + rng.normal(0, 0.003, n))
high = np.maximum(open_, close) * (1 + np.abs(rng.normal(0, 0.006, n)))
low = np.minimum(open_, close) * (1 - np.abs(rng.normal(0, 0.006, n)))
volume = rng.integers(5_000_000, 200_000_000, n)
df = pd.DataFrame({"open": open_, "high": high, "low": low,
"close": close, "volume": volume}, index=dates)
df.index.name = "date"
print(f"已生成模拟数据:{dates[0].date()} ~ {dates[-1].date()},共 {n} 个交易日")
return df
df = load_data()
第二个代码块:探测可用的指标库,并用它算五个指标:
import numpy as np
import pandas as pd
# —— 第一步:探测当前环境里哪个库能用,保证三种环境都能跑 ——
lib_name = None
try:
import talib # TA-Lib 的 Python 封装,快但安装门槛高
lib_name = "TA-Lib"
except ImportError:
try:
import pandas_ta as ta # 纯 Python 实现,pip 一步到位
lib_name = "pandas-ta"
except ImportError:
lib_name = None # 两个都没有,稍后只展示手写版
print("当前使用的指标库:", lib_name or "都没有,只展示手写版")
def lib_indicators(df):
"""用检测到的指标库算五个常用指标,统一返回 {名字: Series} 字典。"""
close = df["close"].to_numpy(dtype="float64") # TA-Lib 只认 float64 数组
high = df["high"].to_numpy(dtype="float64")
low = df["low"].to_numpy(dtype="float64")
out = {}
if lib_name == "TA-Lib":
out["SMA20"] = pd.Series(talib.SMA(close, timeperiod=20), index=df.index)
out["EMA20"] = pd.Series(talib.EMA(close, timeperiod=20), index=df.index)
macd, _, _ = talib.MACD(close) # 默认参数 12/26/9,这里只用 MACD 线
out["MACD"] = pd.Series(macd, index=df.index)
out["RSI14"] = pd.Series(talib.RSI(close, timeperiod=14), index=df.index)
out["ATR14"] = pd.Series(talib.ATR(high, low, close, timeperiod=14), index=df.index)
elif lib_name == "pandas-ta":
out["SMA20"] = ta.sma(df["close"], length=20)
out["EMA20"] = ta.ema(df["close"], length=20)
out["MACD"] = ta.macd(df["close"]).iloc[:, 0] # 三列里的第一列是 MACD 线
out["RSI14"] = ta.rsi(df["close"], length=14)
out["ATR14"] = ta.atr(df["high"], df["low"], df["close"], length=14)
return out
第三个代码块:手写复算,再逐个指标核对一致性:
import numpy as np
import pandas as pd
def hand_indicators(df):
"""用 pandas 手写同样的五个指标(第 10~14 篇的做法收拢成一个函数)。"""
close, high, low = df["close"], df["high"], df["low"]
out = {}
out["SMA20"] = close.rolling(20).mean() # 简单均线
out["EMA20"] = close.ewm(span=20, adjust=False).mean() # 指数均线
ema_fast = close.ewm(span=12, adjust=False).mean()
ema_slow = close.ewm(span=26, adjust=False).mean()
out["MACD"] = ema_fast - ema_slow # MACD 线 = 快慢 EMA 之差
delta = close.diff()
gain = delta.clip(lower=0).ewm(alpha=1 / 14, adjust=False).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1 / 14, adjust=False).mean()
out["RSI14"] = 100 - 100 / (1 + gain / loss) # Wilder 平滑版 RSI
prev_close = close.shift(1)
tr = pd.concat([high - low, (high - prev_close).abs(),
(low - prev_close).abs()], axis=1).max(axis=1) # 真实波幅
out["ATR14"] = tr.ewm(alpha=1 / 14, adjust=False).mean() # Wilder 平滑版 ATR
return out
lib = lib_indicators(df)
hand = hand_indicators(df)
# —— 一致性核对:跳过前 250 根预热期,再比每个指标的最大绝对差 ——
print("\n一致性核对(从第 250 根 bar 开始比较):")
for name in ["SMA20", "EMA20", "MACD", "RSI14", "ATR14"]:
if name not in lib:
continue
diff = (lib[name] - hand[name]).iloc[250:].abs().max()
verdict = "一致" if diff < 1e-4 else "有差异,先查口径"
print(f" {name:>6}: 最大差异 = {diff:.6f} -> {verdict}")
print("\n最新一根 bar 的指标值:")
for name, s in (lib or hand).items(): # 库没装就打印手写版
print(f" {name:>6} = {s.iloc[-1]:.3f}")
代码逐段解释
- 库探测:两层 try-import 组成一条「降级链」——先试 TA-Lib,再试 pandas-ta,都不行就置空。后面所有代码通过
lib_name分支,任何环境都能跑出结果,这正是「装不上也有退路」的落地写法。 - lib_indicators 函数:两个分支的输出统一成「名字 → Series」的字典。统一格式的收益在第三块看得见:核对代码完全不关心你用的是哪个库。
- hand_indicators 函数:把第 10~14 篇的手写实现收拢。RSI 和 ATR 用
alpha=1/14的 Wilder 平滑,对应递推公式X_today = X_yesterday + (1/14) × (x_today − X_yesterday)。公式白话:今天的平滑值等于昨天的平滑值再往今天的实际值方向挪 1/14 的距离,越老的数据影响越小。 - 为什么从第 250 根 bar 开始比:SMA 是滚动平均,谁算都一样,从头就该一致;但 EMA、RSI、ATR、MACD 是递推指标,第一颗「种子」各家给法不同,前几十根 bar 会有可见差异,之后每根 bar 误差乘一次
(1 − 1/14)——大约一年后就只剩亿分之一。所以「预热期对不上」不是 bug,是数学性质。 - 结果解读:SMA 的最大差异应为 0.000000;其余四项在预热期之后也应收敛到万分之一以内。若某项差异大,先查参数口径(周期、乘数、复权方式)是否一致,再回头查代码。
示例输出(示意,具体数值取决于你拉到的数据与装了哪个库):
当前使用的指标库: TA-Lib
已加载 akshare 真实数据:平安银行,共 1215 个交易日
一致性核对(从第 250 根 bar 开始比较):
SMA20: 最大差异 = 0.000000 -> 一致
EMA20: 最大差异 = 0.000001 -> 一致
MACD : 最大差异 = 0.000003 -> 一致
RSI14: 最大差异 = 0.000018 -> 一致
ATR14: 最大差异 = 0.000037 -> 一致
最新一根 bar 的指标值:
SMA20 = 10.852
EMA20 = 10.837
MACD = 0.026
RSI14 = 54.318
ATR14 = 0.194
划重点:两个实现对不上,先怀疑预热期和参数口径,再怀疑代码写错。绝大多数「指标 bug」其实是口径没对齐,而不是谁算错了。
常见坑与小练习
坑 1:uv pip install ta-lib 报 Visual C++ 相关的编译错误。 现象是安装中途报错,提示缺少编译器或找不到 C 库。原因是本机没有 TA-Lib 的 C 内核。避免方式:按本篇「三条路」依次尝试——新版预编译轮子、手动补 C 库、换 pandas-ta。
坑 2:import pandas_ta 报 cannot import name 'NaN' from 'numpy'。 原因是旧版 pandas-ta(0.3.14b0)与 NumPy 2.x 不兼容。避免方式:执行 uv pip install "numpy<2" 降级 NumPy,或安装 pandas-ta 的 GitHub 开发版。
坑 3:对比前 20 根 bar,发现库和手写「对不上」。 原因是递推指标的初始种子不同,属于正常现象而非 bug。避免方式:永远跳过预热期再比较,本篇从第 250 根开始。
坑 4:TA-Lib 函数传入 int64 数组直接报错。 TA-Lib 只接受 float64。避免方式:养成习惯,传参前统一 to_numpy(dtype="float64")。
坑 5:pandas-ta 返回列名带参数后缀,硬编码取列取错。 比如把 MACDh_12_26_9(柱状图)当成 MACD_12_26_9(MACD 线)。避免方式:先 print(result.columns) 确认,或按位置 .iloc[:, 0] 取列。
小练习 1:把 RSI 周期改成 6 和 24,分别做库与手写的一致性核对,观察哪种周期预热期更长。
提示:周期越长,递推收敛越慢,建议把对比起点从第 250 根往后挪(比如第 500 根)再看。
小练习 2:用 pandas-ta 的 ta.bbands 算布林带,和第 13 篇的手写布林带(MA20 ± 2 倍标准差)对比上轨。
提示:
ta.bbands一次返回 5 列,列名形如BBL_20_2.0、BBU_20_2.0,上轨是 BBU 开头那列。
小练习 3:给 lib_indicators 和 hand_indicators 各加一个 OBV(第 15 篇写过手写版),再核对一致性。
提示:TA-Lib 写法是
talib.OBV(close, volume);手写版是np.sign(close.diff()).fillna(0)逐日乘成交量后累加。
本文策略仅用于教学,不构成投资建议,实盘前请充分测试。