ta / 预热窗口与 NaN
ta 指标前面为什么是 NaN:43 个类的预热长度实测表
把 OHLCV 喂进 ta 之后,第一件让人困惑的事是「为什么结果前面一堆空值」。答案是
滚动窗口还没填满:RSIIndicator(window=14) 需要 14 根 K 线才出第一个值,
所以前 13 行是 NaN。
但麻烦在于每个指标的预热长度都不一样——本站实测从 0 行(OBV、ADI)
到 47 行(STC、Mass Index)。更麻烦的是,ATR 和 ADX 的表头不是 NaN 而是 0,
用 isna() 根本筛不掉。这一页给出全部 43 个类的实测数字。
三种机制
ta 的缺失值有三种来源,别当成同一回事
| 机制 | 哪些指标 | 表头表现 | 怎么判断可用性 |
|---|---|---|---|
| 滚动窗口未满 | 大部分趋势/动量/波动率指标(SMA、RSI、CCI、BB、DC、UI…) | NaN,长度 = 窗口 − 1 | 用 isna() 或 first_valid_index() |
| 多级窗口叠加 | MACD、PPO、PVO、TSI、StochRSI、STC、Mass Index、Aroon | NaN 更长:MACD 信号线 33 行、TSI 37 行、STC 与 Mass Index 47 行 | 同上,但要知道「基础指标出值 ≠ 派生列出值」 |
| 数组零初始化 | AverageTrueRange、ADXIndicator | 0.0 而不是 NaN | isna() 无效;要按窗口手动切片或把 0 换掉 |
| 平滑类无预热 | OBV、ADI、NVI、Daily Return、Cumulative Return | 几乎立即出值(0 或 1 行 NaN) | 可直接使用,但要理解它们的基准点 |
为什么会有 0 值表头:看 AverageTrueRange._run() 的实现就能看到——它先用 np.zeros(len(close)) 建数组,再从第 window-1 位开始用递归公式填充。前 window-1 位从来没被赋值,自然就是 0。这和「NaN 表示缺失」的常见约定完全相反。
预热长度表
43 个指标类各自的预热长度是多少
| 类别 | 类名 | 最长预热 | 对应输出列 | 特殊情况 |
|---|---|---|---|---|
| 趋势 | PSARIndicator | 212 | psar_up | 表头为 0:psar_down_indicator、psar_up_indicator |
| 趋势 | MassIndex | 47 | mass_index | 输出 1 条 |
| 趋势 | STCIndicator | 47 | stc | 输出 1 条 |
| 趋势 | TRIXIndicator | 40 | trix | 输出 1 条 |
| 动量 | TSIIndicator | 37 | tsi | 输出 1 条 |
| 动量 | AwesomeOscillatorIndicator | 33 | awesome_oscillator | 输出 1 条 |
| 动量 | PercentagePriceOscillator | 33 | ppo_hist | 输出 3 条 |
| 动量 | PercentageVolumeOscillator | 33 | pvo_hist | 输出 3 条 |
| 趋势 | MACD | 33 | macd_diff | 输出 3 条 |
| 动量 | StochRSIIndicator | 30 | stochrsi_d | 输出 3 条 |
| 动量 | UltimateOscillator | 28 | ultimate_oscillator | 输出 1 条 |
| 趋势 | IchimokuIndicator | 25 | ichimoku_a | 输出 4 条 |
| 动量 | StochasticOscillator | 15 | stoch_signal | 输出 2 条 |
| 动量 | ROCIndicator | 14 | roc | 输出 1 条 |
| 趋势 | AroonIndicator | 14 | aroon_down | 输出 3 条 |
| 趋势 | KSTIndicator | 14 | kst | 输出 3 条 |
| 趋势 | VortexIndicator | 14 | vortex_indicator_diff | 输出 3 条 |
| 成交量 | EaseOfMovementIndicator | 14 | sma_ease_of_movement | 输出 2 条 |
| 成交量 | ForceIndexIndicator | 14 | force_index | 输出 1 条 |
| 动量 | KAMAIndicator | 13 | kama | 输出 1 条 |
| 动量 | RSIIndicator | 13 | rsi | 输出 1 条 |
| 动量 | WilliamsRIndicator | 13 | williams_r | 输出 1 条 |
| 趋势 | CCIIndicator | 13 | cci | 输出 1 条 |
| 趋势 | DPOIndicator | 13 | dpo | 输出 1 条 |
| 趋势 | EMAIndicator | 13 | ema_indicator | 输出 1 条 |
| 趋势 | SMAIndicator | 13 | sma_indicator | 输出 1 条 |
| 趋势 | WMAIndicator | 13 | wma | 输出 1 条 |
| 波动率 | BollingerBands | 13 | bollinger_hband | 表头为 0:bollinger_hband_indicator、bollinger_lband_indicator |
| 波动率 | DonchianChannel | 13 | donchian_channel_hband | 输出 5 条 |
| 波动率 | KeltnerChannel | 13 | keltner_channel_mband | 表头为 0:keltner_channel_hband_indicator、keltner_channel_lband_indicator |
| 波动率 | UlcerIndex | 13 | ulcer_index | 输出 1 条 |
| 成交量 | ChaikinMoneyFlowIndicator | 13 | chaikin_money_flow | 输出 1 条 |
| 成交量 | MFIIndicator | 13 | money_flow_index | 输出 1 条 |
| 成交量 | VolumeWeightedAveragePrice | 13 | volume_weighted_average_price | 输出 1 条 |
| 成交量 | VolumePriceTrendIndicator | 1 | volume_price_trend | 输出 1 条 |
| 其他 | DailyLogReturnIndicator | 1 | daily_log_return | 输出 1 条 |
| 其他 | DailyReturnIndicator | 1 | daily_return | 输出 1 条 |
| 趋势 | ADXIndicator | 0 | adx | 表头为 0:adx、adx_neg、adx_pos |
| 波动率 | AverageTrueRange | 0 | average_true_range | 表头为 0:average_true_range |
| 成交量 | AccDistIndexIndicator | 0 | acc_dist_index | 输出 1 条 |
| 成交量 | NegativeVolumeIndexIndicator | 0 | negative_volume_index | 输出 1 条 |
| 成交量 | OnBalanceVolumeIndicator | 0 | on_balance_volume | 输出 1 条 |
| 其他 | CumulativeReturnIndicator | 0 | cumulative_return | 表头为 0:cumulative_return |
读法:「最长预热」是该类所有输出列里前导 NaN 最多的那一条(也就是整类可用所需的最小行数)。按这份实测表,要保证全部 43 个类都出值,样本至少需要 448 行(400 行样本 + 47 行预热);只想用 RSI 的话 14 行就够。数字来自 400 行合成 OHLCV(固定随机种子),以你实际数据的长度与缺口情况为准。
fillna 语义
fillna=True 到底填了什么:数值级验证
| 对比项 | fillna=False(默认) | fillna=True |
|---|---|---|
| 布尔带中轨(window=20)前导 NaN | 19 行 | 0 行 |
| 第 6 行(索引 5)的值 | NaN | 102.600107 |
| 同一位置的真实含义 | 窗口未满,不该有值 | = 该标的前 6 根 K 线收盘价的算术平均(本机复算:102.600107) |
| 第 20 行(索引 19)的值 | 102.366137 | 102.366137(与默认一致) |
| 实现机制 | Pandas rolling 的 min_periods=window | min_periods=0——窗口不满也照样算 |
| 用在特征工程里 | 得到带 NaN 的列,需要自行 dropna 或填充 | 得到无 NaN 的列,但头部若干行是「用 1 根、2 根…K 线算出来的」 |
这是本站认为最值得记住的一条:fillna=True 不是「用均值/前值把 NaN 补上」,而是让窗口在不完整时就开始计算。它确实消灭了 NaN,代价是头部若干行的统计含义与后续行不同——一个 20 日均线的前 19 个值,分别是用 1、2、…、19 根 K 线算出来的。把这类列直接喂进模型或回测,等于在头部混入了尺度不同的样本。
零值表头
哪些指标的表头是 0 而不是 NaN
| 输出列 | 所属类 | 表头行为 | 为什么 | 建议 |
|---|---|---|---|---|
average_true_range() | AverageTrueRange | 前 13 行 0.0,全列无 NaN | 数组零初始化 + 递归填充 | 当成缺失值处理,别参与计算 |
adx() / adx_pos() / adx_neg() | ADXIndicator | 无 NaN,但表头 15–27 行为 0 | 同类实现方式 | 同上;注意 ADX 的 0 会被误读成「无趋势」 |
bollinger_hband_indicator() / lband_indicator() | BollingerBands | 只有 0 / 1,前 30 行为 0 | 这是信号标记列,不是价格 | 0 是合法取值,别当缺失值删 |
psar_up_indicator() / psar_down_indicator() | PSARIndicator | 只有 0 / 1 | 同上(标记列) | 同上 |
keltner_channel_kchi/kcli | KeltnerChannel | 只有 0 / 1 | 同上(标记列) | 同上 |
on_balance_volume() / acc_dist_index() / negative_volume_index() | 成交量模块 | 从 0 累加,所以首值接近 0 不是缺失 | 累积量指标以某点为基准 | 看差分或变化率,别直接比绝对值 |
区分两类 0:一类是「初始化留下的 0」(ATR、ADX)——那是缺失值,必须处理;另一类是「标记列的 0」(各种 *_indicator)——那是合法的信号取值,删掉就丢了信息。两种 0 在同一张表里并存,这就是为什么不能无脑 df != 0。
dropna 行为
ta.utils.dropna() 会删掉哪些行
| 调用方式 | 输入行数 | 输出行数 | 发生了什么 |
|---|---|---|---|
ta.utils.dropna(df)(数据干净、无 0 无缺失) | 400 | 400 | 原样返回 |
| 同上,但某一天 volume = 0(例如停牌) | 400 | 399 | 该行被整行删除——0 被当成缺失值 |
| 同上,但某一天 close = 0(数据源异常) | 400 | 399 | 该行被删除(这一种是期望行为) |
| 同上,但某一天 volume = NaN | 400 | 399 | 该行被删除 |
dropna() 用在 add_all_ta_features 的输出上 | 400 | 0 | 86 列里每一行都至少有一列是 NaN,于是全部行被删光 |
源码里有两次「隐蔽的替换」:dropna() 先执行 df[number_cols] = df[number_cols][df[number_cols] < math.exp(709)] 和 df[number_cols] = df[number_cols][df[number_cols] != 0.0],第二步把所有 0 值变成 NaN,然后才 dropna()。官方 README 的用法是「先 dropna 清洗原始数据,再 add_all_ta_features」——顺序反了会把整个数据集删空。
处理策略
NaN 该怎么处理:按目的选做法
| 你的目的 | 推荐做法 | 代价 / 注意点 |
|---|---|---|
| 接进机器学习管道 | 用默认 fillna=False,然后按最长预热行数整体截掉头部 | 会损失 47 行样本;换指标组合可以减少损失(只有 STC/Mass Index 需要 47 行) |
| 只想快速看某只标的现状 | 用 fillna=True,头部值不要当真 | 头部若干行统计含义不同,不要拿去做结论 |
| 回测 | 用 fillna=False,并对每个指标单独按其预热长度对齐起点 | 不同指标起点不同,信号时间必须取最晚的那个,否则会用未来数据 |
| 处理停牌 / 无成交日 | 不要用 ta.utils.dropna();自己按规则过滤 | 该函数会把合法的 0 成交量行删掉,造成时间序列断裂 |
| 只想用一个指标 | 直接用该指标的类,跳过 wrapper | 需要自己知道它的预热长度(本页表格里有) |
一条通用原则:先确定样本的可用起点,再开始算指标。把 43 个指标一起算、再用 dropna() 收尾,会同时遇到「删空」和「起点不一致」两个问题。回测场景下起点不一致还可能引入未来数据,详见回测边界页。想先看每个指标的窗口与输出列,去指标目录;要一次性拿到 86 列,看批量特征页。
FAQ
关于 ta 预热窗口与 NaN 的常见问题
为什么 ta 算出来的指标前面是 NaN?
fillna=False 时,Pandas 的 rolling 用 min_periods=window,前 window-1 行就是 NaN。RSIIndicator(window=14) 前 13 行是 NaN,第 14 行起有值。以官方实现与你实际窗口参数为准。fillna=True 是不是就是把 NaN 填成 0 或均值?
fillna=True 只是把 min_periods 从窗口长度改成 0,让窗口不满也照常计算。所以 20 日布林带中轨的前 19 个值,分别是用 1、2、…、19 根 K 线的均值——数值上等于「部分窗口均值」,不是常数填充。本机复算第 6 行等于前 6 根收盘价均值,完全吻合。ATR 算出来全是 0 是 bug 吗?
AverageTrueRange._run() 用 np.zeros(len(close)) 初始化,再从第 window-1 位开始递归填充,所以前 window-1 位是 0.0 而不是 NaN。ADXIndicator 有同样的特点。把这些 0 当真实值参与计算会得到错误的仓位或止损水平。ta.utils.dropna() 为什么把我一半数据删了?
volume=0,输出就从 400 行变成 399 行。用它之前先确认数据里没有合法的 0 值;以你实际的 ta.utils.dropna 源码版本为准。样本要准备多少行才够?
STCIndicator 与 MassIndex),所以要全部指标都出值,至少需要 47 + 1 行有效数据;实践中本站在 400 行样本上跑完了全部指标。只用 RSI 的话 14 行就够。样本长度以你的指标组合与官方实现为准。不同指标的 NaN 起点不同,能不能直接对齐全表?
这些 NaN 长度会随版本变化吗?
__init__ 签名里(本页数字取的是本机 ta 0.11.0 的默认值)。升级版本或自己传不同的 window 后,请以你的实际输出为准重新测量。