Skip to content

Release v0.8.1 - #769

Merged
mrjbq7 merged 12 commits into
masterfrom
dev-v0.8.1
Sep 21, 2026
Merged

mrjbq7 merged 12 commits into
masterfrom
dev-v0.8.1

Conversation

@mario4tier

Copy link
Copy Markdown
Member

Tested against the released TA-Lib C v0.8.1. Carries the compatibility changes for it and the new streaming API.

The dev line requires a TA-Lib C that has no release yet, so a PR into dev could
not build -- and in fact ran nothing at all, since tests.yml triggers on master
only. This adds tests-dev.yml, which triggers on dev, builds the C library from
source at the tip of its dev branch and caches it by commit, then runs the same
steps tests.yml does. tests.yml is untouched: master keeps installing the
published release.

It resolves the commit with git ls-remote rather than api.github.com, which is
rate limited per runner IP and answers 403 often enough to redden a job for a
reason unrelated to the code under test.
talib.stream.SMA(close) returns a handle, not a value: .value is the value at
the last history bar, .update(bar) is O(1) and returns that bar's, .peek(bar)
evaluates a forming bar without committing, .copy() forks it, and
.open_and_fill() returns the handle plus the Function API's series in one pass.
Multi-output functions answer with a named tuple; .out_range and .advance()
carry the C contract's bar range.

BREAKING: talib.stream_X and the old value-returning talib.stream.X are gone,
along with the stream_* stubs in _ta_lib.pyi -- talib/stream.pyi types the
handles instead. Migrating is stream.X(...) -> stream.X(...).value, and the
compiler will not find the sites: `if stream.CDLDOJI(o, h, l, c):` used to test
the pattern and now tests a handle, which is always true.

Adds talib.InsufficientHistory, raised when an open gets too little history.

talib/_stream.pxi and talib/stream.pyi are generated by
tools/generate_stream.py (--stub for the second).
SUPERTREND's streaming outputs are outSupertrend/outTrend in the C header.
The committed _stream.pxi predates that rename and names them real/integer,
so its namedtuple fields disagree with abstract's output_names and
test_open_and_fill_matches_batch[SUPERTREND] fails on dev.

_ta_lib.c regenerated with Cython 3.2.8, the version that produced the
committed file.

Claude-Session: https://claude.ai/code/session_01RQsaLFjtMXaVE4UHQKVaeu
update() on a multi-output handle spent ~125 ns/bar in namedtuple.__new__
against a 6 ns C call. MACD update() goes from 182 to 57 ns/bar with every
candidate measured in one binary, and from 170 to 37 ns/bar end to end;
BBANDS, STOCH, AROON and MINMAXINDEX all land in the -73% to -75% band.

The Function API already returns a plain tuple, so the two tiers now agree.
tuple -> NamedTuple stays available later as a non-breaking upgrade, since
every tuple idiom keeps working; the reverse would not be.

Single-output handles are untouched: 178 of 178 single-output classes are
byte-identical before and after.

test_open_and_fill_matches_batch loses its _fields check and gains a type
and arity check that reaches all 23 multi-output and 178 single-output
functions; order stays pinned by the element-wise comparison above it.

Claude-Session: https://claude.ai/code/session_01RQsaLFjtMXaVE4UHQKVaeu
Cython emitted 58 warnings, all of the form

  Global name __PANDAS_SERIES matched from within class scope in contradiction
  to Python 'class private name' rules. This may change in a future release.

The Function class reads seven module globals whose leading double underscore
makes them mangling-eligible, so Python's own rules say those references should
resolve to _Function__PANDAS_SERIES and fail. Cython resolves them to the module
global instead and warns that it may stop. Dropping one underscore makes the
code mean what it already did.

The seven are private by every convention -- absent from __all__, undocumented --
and __TA_FUNCTION_NAMES__ is untouched: its trailing underscores put it outside
the mangling rule. tools/generate_stream.py referenced them too, so _stream.pxi
is regenerated.

Nothing else moves. dir() of talib, talib._ta_lib, talib.abstract and
talib.stream differs only by those seven names, and the generated C, normalised
for the rename, differs only in the interned string table -- no function body,
no ABI, no code path.
v0.8.0 shipped without the streaming API, so its entries move out of the
released section.

Claude-Session: https://claude.ai/code/session_01TuVjx2Q5urdmwi5opzfCp8
TA-Lib C 0.8.1 released SUPERTREND with outputs outSupertrend and outTrend.
_func.pxi was generated against a pre-release C that still called them
outReal and outInteger, so help(talib.SUPERTREND) and the abstract stub
listed "real" and "integer", while abstract.Function('SUPERTREND').output_names
already reported the released names at runtime.

_ta_lib.pxd now declares the header's parameter names, and _func.pxi,
abstract.pyi and _ta_lib.c are regenerated against the released 0.8.1
header. Return values, dtypes and argument order do not change.

Claude-Session: https://claude.ai/code/session_01TuVjx2Q5urdmwi5opzfCp8
An empty array made the Function API call TA-Lib with pointers one element
before its buffers: check_begidx answers length - 1, which is -1, so the
wrapper passed startIdx = endIdx = 0 and data + begidx. A function whose
lookback is zero reads and writes that element. That is 34 functions at
their default parameters, ACOS, ADD and OBV among them, plus any function
whose parameters give a zero lookback, such as MA(x, timeperiod=1), through
the Function, Abstract, pandas and polars paths alike. The heap corruption
may abort the interpreter later, on an unrelated free, or go unnoticed.
Stream opens are not affected; they raise InsufficientHistory first.

When it started: 0.4.27, tagged 2023-07-13. Commit 02de3a3 ("don't throw
exceptions on all nan input", CHANGELOG "[FIX]: Don't throw exceptions when
inputs are all NaN") replaced check_begidx's `raise Exception("inputs are
all NaN")` with `return length - 1`. An empty array takes that same branch,
so until then it raised; since then it has reached TA-Lib with begidx -1.
Every release since has it, through 0.8.0.

Every wrapper now returns its outputs as empty arrays, before check_begidx,
the lookback or TA-Lib are reached. Nothing else changes for an empty input:
types, dtypes and tuple layout match the non-empty path, and for valid
parameters the results are identical to before across func, the pandas and
polars wrappers, and abstract with dict, pandas and polars DataFrame inputs,
for all 201 functions. An invalid parameter on an empty input now returns
empty outputs instead of TA_BAD_PARAM: there is nothing to compute.

The test runs in a child process with the empty inputs right after an
inaccessible page, so a read before the buffer faults at once. It fails on
the previous code with SIGSEGV. It is skipped on Windows, where the guard
would need VirtualProtect and no CI runs the tests.

Claude-Session: https://claude.ai/code/session_01TuVjx2Q5urdmwi5opzfCp8
dev tracks TA-Lib C's dev branch, whose version moves ahead of the release.
@mrjbq7
mrjbq7 merged commit e5aab28 into master Sep 21, 2026
18 checks passed
@mrjbq7

mrjbq7 commented Sep 21, 2026

Copy link
Copy Markdown
Member

Awesome!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants