Skip to content

feat(stream): handles over TA-Lib C 0.8.1's streaming API (#757) - #759

Merged
mario4tier merged 1 commit into
devfrom
feat/streaming-handles
Sep 6, 2026
Merged

feat(stream): handles over TA-Lib C 0.8.1's streaming API (#757)#759
mario4tier merged 1 commit into
devfrom
feat/streaming-handles

Conversation

@mario4tier

Copy link
Copy Markdown
Member

Implements #757: talib.stream becomes handles over TA-Lib C 0.8.1's streaming
API, replacing the last-value functions.

from talib import stream

s = stream.RSI(history, timeperiod=14)   # same arguments as talib.RSI
s.value                                  # == talib.RSI(history)[-1], bitwise

for bar in feed:
    emit(s.update(bar))                  # O(1) per closed bar

s.peek(forming)                          # what update would return; commits nothing
fork = s.copy()                          # independent handle at the same bar
s, rsi = stream.RSI.open_and_fill(history, timeperiod=14)   # both in one pass

Multi-output functions answer with a named tuple whose fields are the output
names in the docstring; single-output ones with a bare float (or int).
out_range and advance() carry the C contract's bar range. New
talib.InsufficientHistory for an open with too little history.

What to read

Two files: tools/generate_stream.py (the generator) and
tests/test_stream.py (the suite). talib/_stream.pxi, talib/stream.pyi
and talib/_ta_lib.c are generated output — make generate plus make cython
reproduces them byte for byte, so the 275k-line diff is 1000 lines of review.

Breaking change

talib.stream_X and the value-returning talib.stream.X are gone, as are 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.

A few decisions worth a second opinion

  • Leading NaN bars are skipped, exactly as the Function API's check_begidx
    skips them, so stream.X(h).value == talib.X(h)[-1] holds bitwise. The offset
    is added back to out_range and to the MAXINDEX/MININDEX/MINMAXINDEX
    outputs, which the batch tier also rebases.
  • out_range and advance() are exposed although refactor(streaming): replace with the v0.8.1 Core implementation #757 does not name them;
    they are part of the C streaming contract and the other backends have them.
  • open_and_fill returns (handle, outputs), outputs being exactly what the
    batch call returns, pandas/polars container included.
  • No close()__dealloc__ frees, as GC suffices for a managed handle.
  • .github/workflows/tests.yml gains dev in both branch filters; it
    triggered on master only, so a PR into dev ran no checks at all. If this PR
    shows none, that filter needs to land on dev first.

Verification

  • 2100 tests, corpus derived from TA_FUNC_FLG_STREAM: every streaming function
    compared bit-exactly against its batch series over three datasets (real prices,
    prices mapped into [-1, 1], and one with zero-range and zero-volume bars), two
    compatibility modes and two warm-up prefixes, plus a NaN-prefixed corpus.
  • test_the_data_discriminates asserts only candlesticks are output-flat, so the
    comparison cannot quietly go vacuous.
  • Signature parity against the batch function, which is the only check that
    catches an argument-order slip in MEDPRICE/ADD/MULT/WCLPRICE — those are
    bit-identical under a high/low swap.
  • Built and green against ta-lib C dev, including the no-Cython sdist path that
    compiles the committed _ta_lib.c, and a wheel that passes the release test
    command.

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).
@mario4tier
mario4tier force-pushed the feat/streaming-handles branch from 1e58076 to 8185430 Compare September 6, 2026 18:32
@mario4tier
mario4tier merged commit 2e1b336 into dev Sep 6, 2026
6 checks passed
@mario4tier
mario4tier deleted the feat/streaming-handles branch September 7, 2026 01:35
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.

1 participant