数据契约与索引顺序
finta 的数据格式:列名其实不挑大小写,但行序错了会算出未来数据
关于输入,README 只说了一句「列名需要小写」。本站实测发现这句偏严——
inputvalidator 装饰器会先把列名统一转小写,因此 Close、HIGH
这类写法都能正常计算。
真正致命的是 README 没写的另一件事:FinTA 按行序(row order)计算,完全不读
DatetimeIndex 的先后。把同一份数据按时间倒序排列后,2017-06-10 的 SMA(14) 从
2471.232500 变成 2612.512786,而后者正好等于
「当天到未来第 13 根 K 线」的收盘均值——最新在前的数据会把未来数据算进当天。
finta/finta.py 中的 inputvalidator 装饰器与本机顺序对照实验(_evidence/order_pandas1.txt)整理的数据契约示意;非官方插图。FinTA · 列名契约
列名到底要不要小写?缺列会报什么错
| 输入情况 | 实测结果 | 说明 |
|---|---|---|
标准小写列名 open/high/low/close/volume | 正常 | README 推荐的写法 |
全大写列名 OPEN/HIGH/LOW/CLOSE/VOLUME | 正常,SMA(14) 结果与标准写法完全一致(6922.3392206307135) | 装饰器在调用前把每一列名 .lower(),因此大小写不敏感 |
混合大小写(只改 Close、High) | 正常,结果同上 | 同上 |
缺 close | LookupError: Must have a dataframe column named "close" | 只报第一个缺的列名,不会一次列全;修饰器按 o→h→l→c→v 的顺序检查 |
缺 high(但 OHLC 类指标只需 close) | 仍然报错 | 凡是使用 @inputvalidator("ohlc") 的方法都要求四列齐全 |
缺 volume,调用 OHLC 类方法(如 TA.SMA) | 正常 | 只有 @inputvalidator("ohlcv") 的方法才要求 volume |
缺 volume,调用 OHLCV 类方法(如 TA.OBV、TA.VWAP) | LookupError: Must have a dataframe column named "volume" | 错误信息里只有列名,没有方法名,排查时要结合 traceback |
| 第一个参数不是 DataFrame | AttributeError: 'int' object has no attribute 'rename' | 装饰器用 isinstance(args[0], DataFrame) 猜数据参数的位置,猜错时报错信息与数据无关 |
所以真正的要求是「列名能被转成小写后匹配」,而不是「必须手写小写」。但本站仍建议统一成小写——因为 TA.SMA(ohlc, column='close') 这类调用会把你传的列名也一起转小写,两种风格混用时容易对不上。
FinTA · 必需列
哪些方法要 volume、哪些只要 OHLC
| 输入要求 | 代表方法 | 数量级 | 缺列时的表现 |
|---|---|---|---|
只要 open/high/low/close | SMA、EMA、RSI、MACD、BBANDS、ATR、ADX、STOCH、WILLIAMS、CCI、CCI 族、KC、DO、ICHIMOKU… | 约 70 个 | 缺任何一列都抛 LookupError |
需要 volume | OBV、WOBV、ADL、CHAIKIN、MFI、VZO、PZO、EFI、CFI、EBBP、EMV、TMF、VPT、FVE、VFI、VWAP、VAMA、EVWMA、EV_MACD、VW_MACD、VIDYA | 约 20 个 | 缺 volume 抛 LookupError;其余四列仍要齐 |
可以用 column= 指定别的价格列 | TA.SMA(ohlc, 14, column='high') 实测可用(最后一根 7103.111206626428) | 带 column 参数的方法 | 指定不存在的列名同样抛 LookupError |
# 最小可用的数据准备(实测通过)
import pandas as pd
from finta import TA
df = pd.read_csv('ohlc.csv') # 至少要有 open/high/low/close
df['date'] = pd.to_datetime(df['date'])
df = df.set_index('date').sort_index() # ← 关键一步:先按时间正序排好
df.columns = [c.lower() for c in df.columns] # 可选:统一小写,便于阅读
print(df.index.is_monotonic_increasing) # 必须是 True,否则先 sort_index()
print(TA.SMA(df, 14).isna().sum()) # 实测:13 —— 前 13 行是预热
第三行 sort_index() 是整页最重要的一步。FinTA 不会替你排序,也不会在顺序不对时给出任何警告——它只是安静地按行滚动,然后给你一个看起来正常的数字。下一节说明这个数字错在哪里。
FinTA · 行序陷阱
行序反了会怎样?可复现的实测数值
| 指标 | 正序(旧→新) | 倒序(新→旧) | 差值 |
|---|---|---|---|
| SMA(14) | 2471.232500 | 2612.512786 | +141.28 |
| SMMA(14) | 2287.526465 | 2591.918313 | +304.39 |
| EMA(20) | 2399.725062 | 2595.150177 | +195.43 |
| WMA(14) | 2610.552400 | 2627.020181 | +16.47 |
| RSI(14) | 69.634141 | 57.314342 | −12.32 |
| ATR(14) | 249.606173 | 334.969890 | +85.36 |
| BBANDS 上轨 | 3007.946180 | 2857.227605 | −150.72 |
| MACD | 257.607570 | 41.307208 | −216.30 |
| OBV | 4620.13 | −272477.49 | 符号都变了 |
# 证明「倒序的结果 = 用了未来数据」
asc = ohlc.sort_index() # 正序
desc = ohlc.sort_index(ascending=False) # 倒序(很多行情接口的默认形态)
print(TA.SMA(asc, 14).loc['2017-06-10']) # 2471.232500 ← 当天往前 14 根
print(TA.SMA(desc, 14).loc['2017-06-10']) # 2612.512786 ← 当天往后 14 根(未来!)
# 2612.512786 正好等于用正序数据算「当天到 D+13」这 14 根的收盘均值:
i = asc.index.get_loc('2017-06-10')
print(asc['close'].iloc[i:i+14].mean()) # 2612.512786 ← 完全一致
# 另外两个判据:
x = asc.copy(); x.index = x.index[::-1] # 只把索引标签反向、行序不动
print(TA.SMA(x, 14).iloc[i]) # 2471.232500 ← 不变,证明看重的是「行序」
z = asc.sample(frac=1.0, random_state=0) # 打乱行序
print(TA.SMA(z, 14).iloc[i]) # 3769.49... ← 变成无意义的数,且不会报错
这是本站相对官方文档最大的增量,也是「指标看起来有效」最常见的成因。国内不少行情接口(尤其网页端与部分 SDK)返回的是「最新一行在前」,直接 pd.DataFrame(rows) 就落进这个坑:算出来的 RSI 低点恰好出现在价格低点——因为那一天的 RSI 本来就用了后面 13 天的数据。处置只有一句:传进去之前 df.sort_index(),并用 is_monotonic_increasing 断言。
FinTA · 边界情形
停牌、缺口、重复日期会怎样影响结果?四类异常实测
| 情形 | 怎么造 | FinTA 的表现 | 风险 |
|---|---|---|---|
| 中间插入全 NaN 的 K 线(模拟停牌补齐) | 把 200–209 行的 OHLC 全设为 NaN | SMA(14) 输出里共 36 个 NaN(10 行自身 + 滑过缺口的 24 行窗口);但 RSI(14) 只有 1 个 NaN——RSI 用 ewm 平滑,在无数据的行上仍会输出数值 | RSI 的曲线在停牌段看起来是连续的,容易被误读 |
| 中间直接删行 | 删掉第 100–119 行 | 不报错,继续算,最后一根的结果与完整数据一致 | 缺口不会被检测;如果删掉的是关键行,中间段指标会悄悄偏移 |
| 重复时间戳 | 把最后一行再 append 一次(875 行) | 不报错,按 875 行计算,最后一根 SMA 由 6922.339221 变成 6837.227305 | 重复行会被当成一根新 K 线,直接改变结果 |
| 只有 RangeIndex、没有日期 | df.reset_index(drop=True) | 正常工作,结果与日期索引版完全一致 | 说明 FinTA 只关心行的顺序,完全不需要时间信息 |
把四条合起来看:FinTA 不对数据质量做任何检查——不排序、不去重、不检测缺口、不因 NaN 报错。它假设你已经把数据整理好了。所以在接数据这一步,「先自己做一次数据体检」比「调对参数」重要得多。
FinTA · A 股适配
A 股数据传给 FinTA 要注意什么
| 问题 | 为什么重要 | 怎么处理 |
|---|---|---|
| 复权口径 | 前复权 / 后复权 / 不复权的收盘价不同,算出的指标自然不同;跨除权日的均线会跳空 | 同一份研究里固定一种口径;跨除权日的区间要在结论里声明口径,不要把三种混用 |
| 停牌日怎么表示 | 停牌当天没有成交,有的数据源补一行 0 成交量的记录,有的是直接缺失 | 如果补了行,指标会把它当成真实价格波动;建议保留缺失并在指标段单独标注(注意 RSI 在 NaN 行仍会输出数值) |
| 涨跌停与一字板 | 涨跌停当天的 high=low=open=close,会让 ATR、布林带宽度等波动率类指标被低估 | 在结论里区分「含涨跌停日」与「剔除后的」两组,或用更长周期摊平 |
| T+1 与收盘价成交假设 | 指标用当日收盘价计算,但当天买入不能当天卖出;把收盘价当成交价在回测里会高估收益 | 回测时把成交价挪到下一个可交易日,并显式写出这个假设 |
| 成交量 vs 成交额 | FinTA 的 OBV、VWAP、MFI 等只看 volume 字段的数值,不知道单位 | 统一用「手」或「股」,并在项目里写清楚;混用单位会让 OBV 的量级失去可比性 |
| 交易日历与缺口 | A 股有长假与调休,日线序列里会出现跨周缺口,滚动窗口会跨过缺口 | 明白窗口是「按行数」而不是「按自然日」;要按自然日对齐需自己重采样 |
需要强调的是:FinTA 本身不提供任何行情数据,也不做复权处理。上面每一条都是「你怎么把数据整理成它要的样子」的问题,不是它的功能。取数与复权要由你自己的数据源负责。
FinTA · 数据源转换
从常见数据源转到 FinTA 要改什么
五步把任意数据源接到 FinTA
df['date'] = pd.to_datetime(df['date'])、df = df.set_index('date').sort_index(),再断言 is_monotonic_increasing 为 True。这一步能挡掉本页最常见的那个坑。Open/High/Low/Close/Volume(FinTA 能自动转小写,但统一之后可读性更好)。droplevel 或单只取一层,否则列名匹配不上、直接抛 LookupError。replace + astype(float),否则滚动计算会静默失败或报类型错误。TA.SMA(df, 14).isna().sum() 应为 13,再用一个已知的简单均线手算值对照——确认数据真的接对了,再开始做指标。| 数据来源 | 典型形态 | 要改什么 | 特别提醒 |
|---|---|---|---|
| CSV 文件(本地或下载) | 日期是一列字符串,数值列可能带引号或千分位 | 转日期 → 设索引 → 排序 → 统一列名 → astype(float) | 行序取决于文件写法,必须显式排序 |
| 一次取多只标的的宽表 | 列变成 MultiIndex(字段 × 代码)或代码前缀列名 | 先按标的分出单表,droplevel 后重命名 | 列名匹配不上时报 LookupError,信息里只有列名 |
| 网页/接口返回的 JSON 列表 | 常见的顺序是「最新一行在最前」 | 构造 DataFrame 后必须排序 | 这正是本页第 3 节那个未来函数坑的高发场景 |
| 带复权因子的数据 | 同一标的有前复权 / 后复权两套收盘价 | 选定一套,并把口径写进研究记录 | 换口径后全部指标都会变,跨口径比较没有意义 |
| 加密/外盘 7×24 数据 | 没有停牌,但有交易所维护窗口与跳空 | 按需要重采样成固定周期(FinTA 自带 utils.resample) | 重采样后行数变化,预热窗口的行数含义也随之变化 |
FAQ
关于 finta 数据契约的常见问题
FinTA 要求列名必须小写吗?
inputvalidator 装饰器会先把所有列名转成小写再匹配,所以 Close、HIGH、混合大小写都能正常计算,结果与标准小写写法完全一致。但统一小写仍然值得做——因为你传 column= 参数时也会被转小写,两种风格混用容易对不上。报 LookupError: Must have a dataframe column named "close" 怎么办?
print(df.columns.tolist()) 看实际列名。常见原因是列名带空格、带单位后缀、或来自多级列索引没有降级。注意这个错误只报第一个缺的列,修完 close 可能还会提示 high——按 o→h→l→c→v 顺序逐个补。为什么算出来的指标和行情软件不一样?
数据里最新的日期在最前面,直接传给 FinTA 会怎样?
2612.512786,这个数正好等于「当天到未来第 13 根」的均值——也就是把未来数据用上了。传进去之前务必 df.sort_index(),并断言 index.is_monotonic_increasing。停牌日要怎么处理?
FinTA 能自己取 A 股行情吗?
相关阅读:源码公式与预热、指标不是策略。本页结论绑定 2026-10-12 的实测环境(Python 3.11.9),数值口径以官方源码为准。