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。这一页给出报错原文与两种修法。

实测 yfinance 1.7.0六列全小写需 DatetimeIndex核验日期 2026-10-10
列名必须全小写 open/high/low/close/volume
索引必须是 DatetimeIndex
长度最长预热 47 行,建议 ≥100 行
取值0 与 NaN 的含义要自己定义清楚
依据 ta 源码与 yfinance 1.7.0 实测整理的输入契约示意;非官方数据规范。

输入契约

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 Closeyfinance 1.7.0 的 download 默认 auto_adjust=True,只给 Close(已经做过拆股/分红调整)默认拿到的已经是被调整过的价格,不要以为它是「原始收盘价」
成交量会被调整吗不会。成交量列不做复权处理量价类指标(OBV、MFI、VWAP)混用两种价格口径时会出现隐性偏差
A 股的前复权 / 后复权ta 自己不区分复权方式,它只看你喂进去的数字同一个指标用前复权与后复权数据算,历史段的数值完全不同

记住一句话:复权口径是你的责任,不是 ta 的。ta 不会告诉你它拿到的是复权价还是原始价,也没有任何参数可以切换。要多口径对比,就自己准备两份 DataFrame 分别算。

接入步骤

从行情数据到第一列指标要走哪些步骤

从取数到第一列指标:五步

  1. 取数:yf.download("AAPL", start=…, end=…);如果要保留未调整价格,加 auto_adjust=False 再自己决定用哪一列。
  2. 压平列:df.columns = df.columns.droplevel(1)(或 df.xs("AAPL", axis=1, level=1))。
  3. 统一小写:df.columns = [c.lower() for c in df.columns]。
  4. 对齐索引:df = df.sort_index();确认没有重复日期(df.index.duplicated().sum() == 0)。
  5. 先验证一列再批量:先算一列 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 维 Seriesprint(type(df["close"]).__name__)输出 Series(不是 DataFrame)
行数够用print(len(df))≥ 你所用指标的最长预热 + 1(全部指标需 ≥48)
没有意外的 0print((df[["close","volume"]] == 0).sum().to_dict())看业务含义:停牌日的 0 成交量是合法的,但要先决定怎么处理

这七行的顺序就是排查顺序。ta 报错时,从「列名 → 列齐不齐 → 索引类型 → 索引顺序 → 维度」往下走,基本能在两分钟内定位,不必去翻指标实现。

报错对照

ta 常见报错有哪些:原文与对应修法

报错原文触发条件修法
ValueError: Data must be 1-dimensional, got ndarray of shape (105, 1) insteadMultiIndex 列上取 df["Close"],拿到的是 DataFrame 不是 Seriesdf.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 是空的?
按概率排序:①列名大小写不匹配;②传进去的是 DataFrame 而不是 Series(MultiIndex 导致);③行数少于 14 行(预热不够);④索引顺序倒置。用本页的自检清单逐条过一遍,通常两分钟内能定位。具体以你的数据实际情况为准。
需要 DatetimeIndex 吗?
技术上不是必须的——ta 用 Pandas 的 rolling/ewm 按行序计算,RangeIndex 也能跑。但如果你的目的是按日期分析或接回测,就必须是 DatetimeIndex 并且升序,否则你会拿到一份「顺序正确但时间错乱」的结果。
Close 和 Adj Close 该用哪个?
取决于你的用途:做长期收益分析通常该用复权价(yfinance 1.7.0 默认 auto_adjust=True,给出的 Close 已经是调整后的);核对某天的实际成交价则要用未调整价格。本站实测这段窗口上两者最大差 2.350891,差异足以让指标数值明显不同。ta 本身不做任何复权处理。
ta 支持 A 股吗?
ta 不认识任何市场——它只处理你给的数字。所以「支不支持 A 股」等价于「你能不能把 A 股数据整理成六列小写 OHLCV 且索引升序」。要注意的是 A 股的复权口径(前复权/后复权)与停牌日的零成交量,以及 T+1 交易制度对回测的影响,这些都发生在 ta 之外。
数据里有缺失值怎么办?
先判断缺失的来源:停牌造成的行缺失、数据源漏抓、还是字段本身为空。ta 不会替你处理,而 ta.utils.dropna() 会把所有 0 值也当成缺失,对含零成交量的数据是危险的。建议自己按业务规则处理后再喂给 ta,详见预热与 NaN 页。

接下来读哪一页?

数据形状对了之后,下一个问题是「算出来的数字对不对」。数值验证页把 RSI、MACD、布林带和 ATR 与手算结果逐项对照。