ta / OHLCV 数据契约
ta 的 OHLCV 数据要求:列名、索引与 yfinance 的两个坑
ta 不提供任何行情接口——它只吃你已经准备好的 DataFrame。所以「ta 能不能用」这个问题, 实际等价于「你能不能把行情整理成 ta 要的形状」。
本站用 yfinance 1.7.0 实测走了一遍,撞到两个必踩的坑:列名首字母大写,
以及 yf.download 返回 MultiIndex 列——后者会让 df["Close"]
拿到一个 DataFrame 而不是 Series,ta 随即抛
ValueError: Data must be 1-dimensional。这一页给出报错原文与两种修法。
输入契约
ta 对输入 DataFrame 有什么要求
| 要求 | ta 的实际行为 | 不满足会怎样 |
|---|---|---|
| 列名 | 六个参数名由你自己传:add_all_ta_features(df, open="open", high="high", …);传进去的字符串必须与 DataFrame 列名完全一致(区分大小写) | KeyError: 'open' |
| 列名惯例 | 官方示例与 wrapper 默认值都用小写;大写 Close 不会自动匹配 | 最常见的一类报错 |
| 必需列 | add_all_ta_features 需要 open/high/low/close/volume 五列;单独调某个类时只需它签名里要的那几列(如 RSIIndicator 只要 close) | 少列 → KeyError |
| 索引 | 任意索引都能算(位置对齐),但要得到「按日期对齐的结果」必须是 DatetimeIndex 且升序 | 顺序反了 → 时间方向错误,指标含义颠倒 |
| 长度 | 至少要有该指标的预热长度 + 1 行;全部指标最长预热 47 行 | 行数不足 → 整列 NaN |
| 重复日期 | ta 不做去重、也不检查日期是否重复,重复索引会让 rolling 把重复行当成两天 | 静默给出错误结果 |
| 列顺序 | ta 不关心列顺序,只按名字取 | 无影响 |
最容易忽略的是最后两条。ta 内部用 Pandas 的 rolling/ewm,它按行序而不是按索引的时间语义计算。所以「日期倒序的 DataFrame」不会报错,但算出来的 RSI 是倒着算的。养成习惯:进 ta 之前先 df.sort_index()。
yfinance 实测
用 yfinance 取数交给 ta 会报什么错:实测原文与两种修法
# 实测环境:yfinance 1.7.0 + pandas 3.0.6 + ta 0.11.0(2026-10-10) import yfinance as yf from ta.momentum import RSIIndicator df = yf.download("AAPL", start="2024-01-02", end="2024-06-01", progress=False) print(df.columns.tolist()) # [('Close', 'AAPL'), ('High', 'AAPL'), ('Low', 'AAPL'), ('Open', 'AAPL'), ('Volume', 'AAPL')] print(type(df["Close"])) # <class 'pandas.core.frame.DataFrame'> <-- 不是一个 Series! RSIIndicator(close=df["Close"], window=14, fillna=False).rsi() # ValueError: Data must be 1-dimensional, got ndarray of shape (105, 1) instead # ---- 修法 A:把 ticker 这一层去掉(推荐,写 CSV 存档也方便)---- a = df.copy() a.columns = a.columns.droplevel(1) # ('Close','AAPL') -> 'Close' a.columns = [c.lower() for c in a.columns] # -> 'open','high','low','close','volume' rsi = RSIIndicator(close=a["close"], window=14, fillna=False).rsi() print(rsi.isna().sum(), round(rsi.iloc[-1], 6)) # 13 67.308413 # ---- 修法 B:直接按 ticker 这一层切出来 ---- b = df.xs("AAPL", axis=1, level=1) b.columns = [c.lower() for c in b.columns] # ---- 用 add_all_ta_features 一次加全部特征 ---- import ta out = ta.add_all_ta_features(a.copy(), open="open", high="high", low="low", close="close", volume="volume", fillna=False) print(len([c for c in out.columns if c not in a.columns])) # 86
| 数据来源 | 取到的列名(实测) | 对 ta 要做的处理 |
|---|---|---|
yf.download("AAPL")(yfinance 1.7.0) | MultiIndex,两层:('Close','AAPL')、('High','AAPL')、('Low','AAPL')、('Open','AAPL')、('Volume','AAPL') | 先去一层(droplevel(1) 或 xs),再全部转小写 |
yf.download(..., auto_adjust=False) | 同上,但多一列 ('Adj Close','AAPL'),共 6 列 | 想用复权价就先决定取 Close 还是 Adj Close,再改名成 close |
yf.Ticker("AAPL").history(...) | 扁平列:Open、High、Low、Close、Volume、Dividends、Stock Splits(7 列) | 只需转小写;多出来的 dividends / stock_splits 不影响 ta |
| 自己准备的 CSV | 取决于你的数据源 | 确保列名全小写、索引为日期且升序 |
为什么 df["Close"] 会拿到 DataFrame:在 MultiIndex 的两层列上,第一层 Close 对应多个 ticker(哪怕只有一个),Pandas 就把结果按 DataFrame 返回。ta 内部要的是 1 维序列,于是报 Data must be 1-dimensional。这个报错看起来像 ta 的问题,实际上是取数那一步少了一层。
复权口径
Close、Adj Close 和复权方式对指标有什么影响
| 问题 | 实测结果 | 对指标的影响 |
|---|---|---|
| Close 和 Adj Close 差多少 | 在 AAPL 2024-01-02~2024-05-31 这段窗口上,两者最大绝对差 2.350891(价格量级约 170–190 美元) | 价差会直接进入所有价格类指标;均线、通道、RSI 的绝对值都会变 |
| 默认会不会给 Adj Close | yfinance 1.7.0 的 download 默认 auto_adjust=True,只给 Close(已经做过拆股/分红调整) | 默认拿到的已经是被调整过的价格,不要以为它是「原始收盘价」 |
| 成交量会被调整吗 | 不会。成交量列不做复权处理 | 量价类指标(OBV、MFI、VWAP)混用两种价格口径时会出现隐性偏差 |
| A 股的前复权 / 后复权 | ta 自己不区分复权方式,它只看你喂进去的数字 | 同一个指标用前复权与后复权数据算,历史段的数值完全不同 |
记住一句话:复权口径是你的责任,不是 ta 的。ta 不会告诉你它拿到的是复权价还是原始价,也没有任何参数可以切换。要多口径对比,就自己准备两份 DataFrame 分别算。
接入步骤
从行情数据到第一列指标要走哪些步骤
从取数到第一列指标:五步
- 取数:
yf.download("AAPL", start=…, end=…);如果要保留未调整价格,加auto_adjust=False再自己决定用哪一列。 - 压平列:
df.columns = df.columns.droplevel(1)(或df.xs("AAPL", axis=1, level=1))。 - 统一小写:
df.columns = [c.lower() for c in df.columns]。 - 对齐索引:
df = df.sort_index();确认没有重复日期(df.index.duplicated().sum() == 0)。 - 先验证一列再批量:先算一列
RSIIndicator(...),确认前导 NaN 是 13 行、末尾有值,再上add_all_ta_features。
第 5 步不是多余的:add_all_ta_features 会一次算 43 个指标,如果列名或索引有问题,你会在 86 列里到处找原因。先用一个指标确认契约通了,再批量算。
自检清单
喂给 ta 之前该自检哪些项
| 自检项 | 一行代码 | 通过标准 |
|---|---|---|
| 列名全小写 | print([c for c in df.columns if c != c.lower()]) | 输出空列表 |
| 六个必需列齐备 | print([k for k in ["open","high","low","close","volume"] if k not in df.columns]) | 输出空列表 |
| 索引是日期 | print(type(df.index).__name__) | 输出 DatetimeIndex |
| 索引升序且唯一 | print(df.index.is_monotonic_increasing, df.index.duplicated().sum()) | 输出 True 0 |
| 列是 1 维 Series | print(type(df["close"]).__name__) | 输出 Series(不是 DataFrame) |
| 行数够用 | print(len(df)) | ≥ 你所用指标的最长预热 + 1(全部指标需 ≥48) |
| 没有意外的 0 | print((df[["close","volume"]] == 0).sum().to_dict()) | 看业务含义:停牌日的 0 成交量是合法的,但要先决定怎么处理 |
这七行的顺序就是排查顺序。ta 报错时,从「列名 → 列齐不齐 → 索引类型 → 索引顺序 → 维度」往下走,基本能在两分钟内定位,不必去翻指标实现。
报错对照
ta 常见报错有哪些:原文与对应修法
| 报错原文 | 触发条件 | 修法 |
|---|---|---|
ValueError: Data must be 1-dimensional, got ndarray of shape (105, 1) instead | MultiIndex 列上取 df["Close"],拿到的是 DataFrame 不是 Series | df.columns = df.columns.droplevel(1) 后再取列 |
KeyError: 'close' | 列名是大写 Close,或列名是 MultiIndex 元组 | 统一转小写;MultiIndex 先去一层 |
KeyError: 'volume' | add_all_ta_features 需要五列,但数据里没有成交量 | 补上成交量列;只想算价格类指标就直接调对应的类 |
整列都是 NaN | 行数少于该指标的预热长度 | 加长样本;或把 window 调小(注意这会改变指标含义) |
AttributeError: module 'ta' has no attribute 'RSIIndicator' | 误以为顶层包导出指标类 | from ta.momentum import RSIIndicator |
| 结果与别的库对不上 | 复权口径或平滑方式不同(不是报错,但更常见) | 先统一数据口径,再比数值;见数值验证页 |
前五条都是「数据形状」问题,第六条才是「算法口径」问题。实际排查中,十条里有九条属于前者——先把形状对齐,再讨论数值差异。形状对了之后,下一个坑是预热期:每个指标前多少行是 NaN,见预热窗口与 NaN;想直接拿到完整列名清单,看指标目录;装不上环境先看安装与版本核验。
FAQ
关于 ta 数据格式的常见问题
ta 需要哪些列?
add_all_ta_features 需要 open、high、low、close、volume 五列(列名全小写);单独调用某个指标类时,只需要它 __init__ 签名里出现的那些列,例如 RSIIndicator 只要 close。以源码签名为准。用 yfinance 取的数据为什么不能直接算?
yf.download 返回的列是首字母大写的;②它返回的是 MultiIndex 列(字段 × ticker),所以 df["Close"] 拿到的是 DataFrame 而不是 Series,ta 会抛 ValueError: Data must be 1-dimensional。先 droplevel(1) 再转小写即可,本站实测修好后 RSI 正常出值。为什么我的 RSI 是空的?
需要 DatetimeIndex 吗?
DatetimeIndex 并且升序,否则你会拿到一份「顺序正确但时间错乱」的结果。Close 和 Adj Close 该用哪个?
auto_adjust=True,给出的 Close 已经是调整后的);核对某天的实际成交价则要用未调整价格。本站实测这段窗口上两者最大差 2.350891,差异足以让指标数值明显不同。ta 本身不做任何复权处理。ta 支持 A 股吗?
数据里有缺失值怎么办?
ta.utils.dropna() 会把所有 0 值也当成缺失,对含零成交量的数据是危险的。建议自己按业务规则处理后再喂给 ta,详见预热与 NaN 页。