Skip to content

set_unstable_period targets the wrong function against TA-Lib >= 0.6.0 (hardcoded enum table) #752

Description

@mario4tier

talib/_common.pxi builds _ta_func_unst_ids from a hardcoded list:

for i, name in enumerate(['ADX', 'ADXR', 'ATR', ..., 'T3', 'ALL']):
    _ta_func_unst_ids[name] = i

That list is the TA-Lib 0.4.0 numbering. The C enum was renumbered in 0.6.0 (TA_FUNC_UNST_IMI inserted at slot 12), so against any TA-Lib >= 0.6.0 everything from slot 12 up targets the wrong function. setup.py already requires >= 0.6.1, so this is the normal install.

Measured against libta-lib.so 0.8.1 (ctypes, using the exact ids the table produces):

set_unstable_period(name, n) id sent actually sets
ADX … HT_TRENDMODE 0–11 correct
KAMA 12 (retired slot — no-op)
MAMA 13 KAMA
MFI 14 MAMA
MINUS_DI 15 (retired slot — no-op)
MINUS_DM 16 MINUS_DI
NATR 17 MINUS_DM
PLUS_DI 18 NATR
PLUS_DM 19 PLUS_DI
RSI 20 PLUS_DM
STOCHRSI 21 RSI
T3 22 (retired slot — no-op)
ALL 23 only T3

So set_unstable_period('RSI', n) has no effect on RSI, and set_unstable_period('ALL', n) sets one function.

Why it went unnoticed for ~2 years: set and get use the same wrong id, so every round-trip test passes. Only a behavioural check catches it — e.g. set_unstable_period(name, k) must add exactly k leading NaNs to that function's output.

Fix — stop hardcoding. _ta_lib.pxd already externs the enum, and Cython takes extern const values from the C header at build time, ignoring the literal written in the .pxd (verified: an enumerator declared = 999 in a pxd compiled to the header's 21). Building the dict from lib.TA_FUNC_UNST_* is therefore correct against every TA-Lib version, with no version detection.

Two things fall out of it:

  • _ta_lib.pxd:50 declares TA_FUNC_UNST_HD_PHASOR (typo for HT_PHASOR). Harmless while unreferenced; referencing it fails the C compile. Its STOCHRSI = 21 / T3 = 21 duplicate and ALL = 22 are also stale, though ignored.

  • ADXR, MFI and STOCHRSI are no longer C enumerators (TA_FUNC_UNST_UNUSED_1/15/22 since 0.6.0), so their ids cannot come from the header. Note their slots were never read even before that — which is why retiring them was not a functional change. ADXR and STOCHRSI do follow their inner indicator, but through the implementation rather than a knob: TA_ADXR_Lookback calls TA_ADX_Lookback and TA_STOCHRSI_Lookback calls TA_RSI_Lookback, so setting 'ADX' / 'RSI' already moves them. Suggestion: keep all three keys as deprecated no-ops that emit a DeprecationWarning naming the real knob. Aliasing them to ADX/RSI instead would turn a long-standing no-op into a call with side effects on other functions.

Happy to open the PR (dict from the externed constants, the pxd typo, and the NaN-count behavioural test).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions