Skip to content

TA-Lib Migration & the Abstract API

QuantWave exposes a TA-Lib abstract-style introspection registry so screeners, no-code UIs, optimizers, and migration tools can drive every indicator generically — discover its inputs, parameters (with defaults), and outputs, then call it — without hardcoding per-indicator signatures.

If you're coming from talib.abstract, the surface will feel familiar.

Discovery

import quantwave as qw

qw.get_functions()          # -> ['ad', 'adx', 'alma', ..., 'wma']  (all indicators)
qw.get_function_groups()    # -> {'Momentum': ['adx', 'cci', ...], 'Overlap': [...], ...}

get_function_groups() is derived from get_functions(), so the union of every group is exactly the function list — no extras, no duplicates. That invariant is enforced by a test, which makes it safe to build catalogs and menus on top of it.

The Function façade

from quantwave.abstract import Function

f = Function("RSI")
f.input_names      # ['close']
f.parameters       # {'timeperiod': 14}
f.output_names     # ['rsi']
f.group            # 'Momentum'
f.info             # {'name': 'RSI', 'group': 'Momentum', 'input_names': [...], ...}

Calling it accepts either named inputs (a dict or a Polars DataFrame) or positional arrays in canonical open, high, low, close, volume order — just like classic talib:

import numpy as np

close = np.asarray(prices, dtype=float)

Function("RSI")(close, timeperiod=14)                       # -> np.ndarray
Function("ATR")({"high": h, "low": l, "close": c})          # named inputs
Function("ATR")(h, l, c, timeperiod=14)                     # positional (H, L, C)

macd, signal, hist = Function("MACD")(close)                # multi-output -> tuple

Values are computed by the same parity-tested Rust plugins that back the Polars .ta namespace, so Function(name)(...) and pl.col(...).ta.<name>(...) return identical results.

Migrating from talib

talib QuantWave
talib.RSI(close, timeperiod=14) qw.talib.RSI(close, timeperiod=14)
talib.get_functions() qw.get_functions()
talib.get_function_groups() qw.get_function_groups()
talib.abstract.Function('RSI') qw.abstract.Function('RSI')
Function('RSI').input_names Function('RSI').input_names
Function('RSI').parameters Function('RSI').parameters
Function('RSI').output_names Function('RSI').output_names

The quantwave.talib module (161 functions covering the classic TA-Lib surface) provides the drop-in uppercase functions; quantwave.abstract provides the generic, metadata-driven access.

Example: a generic screener

Because every indicator is reachable through the registry, a screener can iterate the whole momentum group without knowing any signature ahead of time:

import numpy as np
import quantwave as qw
from quantwave.abstract import Function

def screen(frame: dict, group: str = "Momentum"):
    """Compute every indicator in `group` over `frame` and return the last value."""
    out = {}
    for name in qw.get_function_groups().get(group, []):
        f = Function(name)
        # Only run indicators whose inputs are present in the frame.
        if not set(f.input_names).issubset(frame):
            continue
        try:
            result = f({k: frame[k] for k in f.input_names})
        except Exception:
            continue
        last = result[0][-1] if isinstance(result, tuple) else result[-1]
        out[name] = float(last)
    return out

frame = {"open": o, "high": h, "low": l, "close": c, "volume": v}
signals = screen(frame, "Momentum")

Swap "Momentum" for any key of qw.get_function_groups() (e.g. "Overlap", "Volume", "Candlestick") to sweep a different family.

For the common case of "compute everything a frame supports," skip the manual loop above and use df.ta.all() (below) — it does the same input-satisfiability filtering, but as one batched Polars pipeline.

Bulk-compute: df.ta.all()

df.ta.all() / lf.ta.all() drive the entire registry generically over a Polars DataFrame or LazyFrame in a single lazy pass: every batch-capable indicator whose required input columns are present gets computed and added as new columns, in one collect(). Streaming-only indicators (no batch .ta implementation) and indicators whose inputs aren't satisfiable by the frame (missing OHLCV columns, or multi-series indicators like beta/correl that need arbitrary paired series) are skipped with a recorded reason — nothing raises.

import polars as pl
import quantwave as qw

df = pl.DataFrame({"open": o, "high": h, "low": l, "close": c, "volume": v})

features, manifest = df.ta.all()
# features: df + one column per single-output indicator (e.g. "rsi"),
#           and f"{name}_{field}" per multi-output indicator (e.g. "bbands_upper")
# manifest: {"computed": [...], "skipped": [{"name", "reason"}, ...], "columns": [...]}

lazy_features, manifest = df.lazy().ta.all()   # LazyFrame in -> LazyFrame out (uncollected)

Filter the universe with include/exclude/groups (set algebra: start from all indicators, restrict to groups, restrict further to include, subtract exclude), or override any indicator's parameters per-call via params:

features, manifest = df.ta.all(
    groups=["Momentum", "Volatility"],
    exclude=["mama"],
    params={"rsi": {"timeperiod": 21}},
)

Pass timing=True to add a manifest["timing"] map of per-indicator wall time (computed via a separate pass, so it trades away some of the batched pipeline's parallelism — only pay for it when profiling).

qw.feature_matrix(df, **same_kwargs) is a thin alias returning (df, feature_column_names) for quick ML pipeline wiring, in place of the richer manifest.

See also