Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/tests-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Tests (dev)

# The dev branch tracks TA-Lib C's dev branch, which has no release yet, so this
# builds the C library from source and the two dev lines move together. master
# is tests.yml, which installs the published release; the steps after the C
# build are that file's, and are meant to stay in step with it.

on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
env:
TA_INCLUDE_PATH: ${{ github.workspace }}/ta-lib-c/include
TA_LIBRARY_PATH: ${{ github.workspace }}/ta-lib-c/lib
LD_LIBRARY_PATH: ${{ github.workspace }}/ta-lib-c/lib
steps:
- uses: actions/checkout@v3

# git, not api.github.com: the API is rate limited per runner IP and its 403
# would redden a job for a reason unrelated to the code under test.
- name: Resolve TA-Lib C dev
id: talib_c
run: |
set -euo pipefail
SHA=$(git ls-remote https://github.com/TA-Lib/ta-lib.git refs/heads/dev | cut -f1)
[ -n "$SHA" ] || { echo "::error::cannot resolve ta-lib dev"; exit 1; }
echo "sha=$SHA" >> "$GITHUB_OUTPUT"

- name: Cache TA-Lib C
id: talib_c_cache
uses: actions/cache@v4
with:
path: ta-lib-c
key: ta-lib-c-${{ runner.os }}-${{ steps.talib_c.outputs.sha }}

- name: Build TA-Lib C
if: steps.talib_c_cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
# the exact commit the cache key names, so a cache hit and a build agree
git init -q ta-lib-c-src
git -C ta-lib-c-src remote add origin https://github.com/TA-Lib/ta-lib.git
git -C ta-lib-c-src fetch -q --depth 1 origin ${{ steps.talib_c.outputs.sha }}
git -C ta-lib-c-src checkout -q FETCH_HEAD
cmake -S ta-lib-c-src -B ta-lib-c-build -DCMAKE_BUILD_TYPE=Release \
-DBUILD_DEV_TOOLS=OFF -DCMAKE_INSTALL_PREFIX="$PWD/ta-lib-c"
cmake --build ta-lib-c-build -j"$(nproc)"
cmake --install ta-lib-c-build

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: Install dependencies
run: |
pip install --upgrade pip wheel setuptools
pip install -r requirements_test.txt
pip install flake8

- name: Build cython modules in-place
run: |
python setup.py build_ext --inplace

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 talib --count --select=E9,F63,F7,F82 --show-source --statistics

- name: Test with pytest
run: |
PYTHONPATH=. pytest
16 changes: 14 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
name: Tests

# master and the dev-* release branches test against the TA-Lib C release the
# wheels are built from. dev tracks TA-Lib C's dev branch, in tests-dev.yml.
on:
push:
branches: [ master ]
branches: [ master, 'dev-*' ]
pull_request:
branches: [ master ]
branches: [ master, 'dev-*' ]

jobs:
build:
Expand All @@ -15,8 +17,18 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: TA-Lib C version
id: talib_c
run: |
set -euo pipefail
V=$(sed -n 's/^ TALIB_C_VER: *//p' .github/workflows/wheels.yml)
[ -n "$V" ] || { echo "::error::no TALIB_C_VER in wheels.yml"; exit 1; }
echo "version=$V" >> "$GITHUB_OUTPUT"

- name: Set up TA-Lib
uses: TA-Lib/setup-ta-lib@v1
with:
version: ${{ steps.talib_c.outputs.version }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
0.8.1
=====

- [CHANGE]: ``talib.stream`` is now the real streaming API of TA-Lib C 0.8.1:
``stream.SMA(close)`` returns a handle, not a value. ``handle.value`` is the
value at the last history bar, ``handle.update(bar)`` costs O(1) and returns
that bar's value, ``handle.peek(bar)`` evaluates a forming bar without
committing it, and ``handle.copy()`` forks it. ``stream.SMA.open_and_fill()``
returns the handle and the Function API's series in one pass. A multi-output
function answers with the same tuple the Function API returns. The old
last-value functions -- ``talib.stream.SMA``, ``talib.stream_SMA``, and their
``_ta_lib.pyi`` stubs -- are gone; ``talib/stream.pyi`` types the handles
instead.

Migrating is ``stream.X(...)`` -> ``stream.X(...).value``, and the compiler
cannot find the sites for you: ``if stream.CDLDOJI(o, h, l, c):`` used to test
the pattern and now tests a handle, which is always true.

- [NEW]: ``talib.InsufficientHistory``, raised when a stream is opened with
too little history. It is the library's one recoverable error, so it is
catchable on its own rather than as a bare ``Exception``.

- [FIX]: ``help(talib.SUPERTREND)`` and the ``abstract`` stub named the outputs
``real`` and ``integer``; they are ``supertrend`` and ``trend``, the names
``abstract.Function('SUPERTREND').output_names`` already reported.

- [FIX]: An empty array given to a function whose lookback is zero, such as
``talib.ACOS`` or ``talib.MA(x, timeperiod=1)``, made TA-Lib read and write
one element outside the buffers, which could crash the interpreter later or
corrupt memory silently. An empty input now returns empty outputs without
calling TA-Lib, whatever the function and its parameters. The bug dates from
0.4.27.

0.8.0
=====

Expand Down
10 changes: 7 additions & 3 deletions DEVELOPMENT
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ talib/_ta_lib.pyx
need to use in the above pyx files.

talib/_stream.pxi
This file contains code for interfacing a "streaming" interface to TA-Lib.
This file is generated automatically by tools/generate_stream.py: one handle
class per indicator, over TA-Lib C's streaming API.

talib/stream.pyi
Type stubs for those handles, generated by tools/generate_stream.py --stub.

tools/generate_func.py,generate_stream.py
Scripts that generate and print _func.pxi or _stream.pxi to stdout. Gets information
about all functions from the C headers of the installed TA-Lib.
Scripts that generate and print _func.pxi, _stream.pxi or stream.pyi to stdout.
Gets information about all functions from the C headers of the installed TA-Lib.

If you are interested in developing new indicator functions or whatnot on
the underlying TA-Lib, you must install TA-Lib from git.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ include talib/*.c
include talib/*.pyx
include talib/*.pxd
include talib/*.pxi
include talib/*.pyi
include tests/*.py
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ talib/_func.pxi: tools/generate_func.py
talib/_stream.pxi: tools/generate_stream.py
python3 tools/generate_stream.py > talib/_stream.pxi

generate: talib/_func.pxi talib/_stream.pxi
talib/stream.pyi: tools/generate_stream.py
python3 tools/generate_stream.py --stub > talib/stream.pyi

talib/abstract.pyi: tools/generate_abstract_stub.py
python3 tools/generate_abstract_stub.py > talib/abstract.pyi

generate: talib/_func.pxi talib/_stream.pxi talib/stream.pyi talib/abstract.pyi

cython:
cython talib/_ta_lib.pyx
Expand Down
90 changes: 73 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,24 @@ your-arm64-python -m pip install --no-cache-dir ta-lib

For 64-bit Windows, the easiest way is to get the *executable installer*:

1. Download [ta-lib-0.7.1-windows-x86_64.msi](https://github.com/ta-lib/ta-lib/releases/download/v0.7.1/ta-lib-0.7.1-windows-x86_64.msi).
1. Download [ta-lib-0.8.1-windows-x86_64.msi](https://github.com/ta-lib/ta-lib/releases/download/v0.8.1/ta-lib-0.8.1-windows-x86_64.msi).
2. Run the Installer or run `msiexec` [from the command-line](https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options).

Alternatively, if you prefer to get the libraries without installing, or
would like to use the 32-bit version:

* Intel/AMD 64-bit [ta-lib-0.7.1-windows-x86_64.zip](https://github.com/ta-lib/ta-lib/releases/download/v0.7.1/ta-lib-0.7.1-windows-x86_64.zip)
* Intel/AMD 32-bit [ta-lib-0.7.1-windows-x86_32.zip](https://github.com/ta-lib/ta-lib/releases/download/v0.7.1/ta-lib-0.7.1-windows-x86_32.zip)
* Intel/AMD 64-bit [ta-lib-0.8.1-windows-x86_64.zip](https://github.com/ta-lib/ta-lib/releases/download/v0.8.1/ta-lib-0.8.1-windows-x86_64.zip)
* Intel/AMD 32-bit [ta-lib-0.8.1-windows-x86_32.zip](https://github.com/ta-lib/ta-lib/releases/download/v0.8.1/ta-lib-0.8.1-windows-x86_32.zip)

#### Linux

Download
[ta-lib-0.7.1-src.tar.gz](https://github.com/ta-lib/ta-lib/releases/download/v0.7.1/ta-lib-0.7.1-src.tar.gz)
[ta-lib-0.8.1-src.tar.gz](https://github.com/ta-lib/ta-lib/releases/download/v0.8.1/ta-lib-0.8.1-src.tar.gz)
and:

```shell
tar -xzf ta-lib-0.7.1-src.tar.gz
cd ta-lib-0.7.1/
tar -xzf ta-lib-0.8.1-src.tar.gz
cd ta-lib-0.8.1/
./configure --prefix=/usr
make
sudo make install
Expand Down Expand Up @@ -534,27 +534,83 @@ slowk, slowd = STOCH(inputs, 5, 3, 0, 3, 0, prices=['high', 'low', 'open'])

## Streaming API

An experimental Streaming API was added that allows users to compute the latest
value of an indicator. This can be faster than using the Function API, for
example in an application that receives streaming data, and wants to know just
the most recent updated indicator value.
The Streaming API keeps a handle per indicator instead of recomputing from the
whole array. Opening one costs a pass over the history; every bar after that is
O(1), and each value it produces is identical to the one the Function API
reports for that bar.

```python
import talib
from talib import stream

close = np.random.random(100)

# the Function API
# the Function API: the whole series, from the whole array
output = talib.SMA(close)

# the Streaming API
latest = stream.SMA(close)
# the Streaming API: a handle, positioned at the end of the history
s = stream.SMA(close)
assert s.value == output[-1]

for price in feed:
latest = s.update(price) # one closed bar in, its value out

s.peek(forming) # what update would return, committing nothing
fork = s.copy() # an independent handle at the same bar
```

`stream.SMA` takes exactly the arguments `talib.SMA` takes. A single-output
function answers with a `float` (an `int` where the Function API returns an
integer array); a multi-output one with the same tuple the Function API returns:

```python
m = stream.MACD(close)
macd, macdsignal, macdhist = m.update(price)
m.value[2] # the histogram, last bar

abstract.Function('MACD').output_names # ['macd', 'macdsignal', 'macdhist']
```

Opening needs at least `lookback + 1` bars, which `abstract` knows, and a little
more where a function's seeding does -- so rather than computing the number,
treat a short history as "not yet":

```python
from talib import abstract

need = abstract.Function('RSI', timeperiod=14).lookback + 1 # 15, usually enough

# the latest value is the same as the last output value
assert (output[-1] - latest) < 0.00001
try:
s = stream.RSI(history, timeperiod=14)
except talib.InsufficientHistory:
... # collect more bars
```

Leading bars that are NaN in any input are not history. They are skipped, as the
Function API skips them, and do not count toward the warm-up. A NaN or an
infinity anywhere else in the history is undefined behaviour in TA-Lib C, and
for a few window functions a handle and the Function API do then disagree.

A bar that is not finite is likewise rejected: `update` raises and the handle is
left exactly as it was, neither its value nor its range moved. For a bar you
mean to skip rather than re-feed, say so with `advance()`, or two handles on one
feed drift a bar apart.

If you want the series over the history as well, one pass gives both:

```python
s, rsi = stream.RSI.open_and_fill(history, timeperiod=14) # rsi == talib.RSI(history)
```

A handle also reports the range it has an output for, in the input series'
coordinates, and can be told about a bar it was not fed:

```python
s.out_range # (begidx, nbelement), as the Function API's output
s.advance() # count a skipped bar: the range moves, the value holds
```

A handle points into the TA-Lib C library, so it cannot be pickled or shared
with another process. Keep the history and re-open instead.

## Supported Indicators and Functions 📋

We can show all the TA functions supported by TA-Lib, either as a `list` or
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "TA-Lib"
version = "0.8.0"
version = "0.8.1"
description = "Python wrapper for TA-Lib"
readme = "README.md"
license-files = ["LICENSE"]
Expand Down Expand Up @@ -40,4 +40,4 @@ requires-python = '>=3.9'

[tool.setuptools]
packages = ["talib"]
package-data = {"talib" = ["_ta_lib.pyi", "py.typed", "abstract.pyi"]}
package-data = {"talib" = ["_ta_lib.pyi", "py.typed", "abstract.pyi", "stream.pyi"]}
18 changes: 4 additions & 14 deletions talib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ def wrapper(*args, **kwds):

result = func(*_args, **_kwds)

# check to see if we got a streaming result
first_result = result[0] if isinstance(result, tuple) else result
is_streaming_fn_result = not hasattr(first_result, '__len__')
if is_streaming_fn_result:
return result

# Series was passed in, Series gets out
if use_pl:
if isinstance(result, tuple):
Expand Down Expand Up @@ -116,6 +110,7 @@ def wrapper(*args, **kwds):
_ta_get_unstable_period as get_unstable_period,
_ta_set_compatibility as set_compatibility,
_ta_get_compatibility as get_compatibility,
InsufficientHistory,
__TA_FUNCTION_NAMES__
)
except ImportError as error:
Expand All @@ -141,14 +136,9 @@ def wrapper(*args, **kwds):
setattr(func, func_name, wrapped_func)
globals()[func_name] = wrapped_func

stream_func_names = ['stream_%s' % fname for fname in __TA_FUNCTION_NAMES__]
stream = __import__("stream", globals(), locals(), stream_func_names, level=1)
for func_name, stream_func_name in zip(__TA_FUNCTION_NAMES__, stream_func_names):
wrapped_func = _wrapper(getattr(stream, func_name))
setattr(stream, func_name, wrapped_func)
globals()[stream_func_name] = wrapped_func
from . import stream

__version__ = '0.8.0'
__version__ = '0.8.1'

# In order to use this python library, talib (i.e. this __file__) will be
# imported at some point, either explicitly or indirectly via talib.func
Expand Down Expand Up @@ -400,4 +390,4 @@ def get_function_groups():
"""
return __function_groups__.copy()

__all__ = ['get_functions', 'get_function_groups'] + __TA_FUNCTION_NAMES__ + ["stream_%s" % name for name in __TA_FUNCTION_NAMES__]
__all__ = ['get_functions', 'get_function_groups', 'InsufficientHistory', 'stream'] + __TA_FUNCTION_NAMES__
Loading
Loading