Files
ferro-ta/python/ferro_ta/data/batch.py
T
2026-03-23 23:34:28 +05:30

263 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Batch Execution API — run indicators on multiple series in a single call.
This module provides a 2-D batch API that accepts a 2-D numpy array
(n_samples × n_series) and applies an indicator to every column, returning
a 2-D output array of the same shape.
For the most common indicators — SMA, EMA, RSI — the 2-D path is handled
entirely in Rust (a single GIL release for all columns). The generic
``batch_apply`` is available for other indicators that do not have a Rust
batch implementation.
Functions
---------
batch_sma — SMA on every column of a 2-D array (Rust fast path for 2-D)
batch_ema — EMA on every column of a 2-D array (Rust fast path for 2-D)
batch_rsi — RSI on every column of a 2-D array (Rust fast path for 2-D)
batch_apply — Generic batch wrapper (Python loop) for any arbitrary indicator
Usage
-----
>>> import numpy as np
>>> from ferro_ta.data.batch import batch_sma
>>> data = np.random.rand(100, 5) # 100 bars, 5 symbols
>>> result = batch_sma(data, timeperiod=14)
>>> result.shape
(100, 5)
"""
from __future__ import annotations
from collections.abc import Callable
import numpy as np
from numpy.typing import ArrayLike
from ferro_ta._ferro_ta import (
batch_adx as _rust_batch_adx,
)
from ferro_ta._ferro_ta import (
batch_atr as _rust_batch_atr,
)
from ferro_ta._ferro_ta import (
batch_ema as _rust_batch_ema,
)
from ferro_ta._ferro_ta import (
batch_rsi as _rust_batch_rsi,
)
from ferro_ta._ferro_ta import (
batch_sma as _rust_batch_sma,
)
from ferro_ta._ferro_ta import (
batch_stoch as _rust_batch_stoch,
)
from ferro_ta.indicators.momentum import RSI
from ferro_ta.indicators.overlap import EMA, SMA
__all__ = [
"batch_sma",
"batch_ema",
"batch_rsi",
"batch_apply",
]
def batch_apply(
data: ArrayLike,
fn: Callable[..., np.ndarray],
**kwargs,
) -> np.ndarray:
"""Apply any single-series indicator *fn* to every column of *data*.
This is the generic fallback batch executor — it calls *fn* once per
column in a Python loop. For the common indicators SMA, EMA, and RSI
prefer the dedicated :func:`batch_sma`, :func:`batch_ema`, and
:func:`batch_rsi` functions, which use a Rust-side loop and avoid
per-column Python round-trips.
Parameters
----------
data : array-like, shape (n_samples,) or (n_samples, n_series)
Input data. If 1-D, the function is called directly on the array
and the result is returned without adding a column dimension.
fn : callable
Single-series indicator function (e.g. ``SMA``, ``EMA``, ``RSI``).
It must accept a 1-D array as first positional argument and return
a 1-D array of the same length.
**kwargs
Extra keyword arguments forwarded to *fn* (e.g. ``timeperiod=14``).
Returns
-------
numpy.ndarray
Same shape as *data*. Leading values are ``NaN`` for the warm-up
period, identical to calling *fn* on each column individually.
Examples
--------
>>> import numpy as np
>>> from ferro_ta import SMA
>>> from ferro_ta.data.batch import batch_apply
>>> data = np.random.rand(50, 3)
>>> out = batch_apply(data, SMA, timeperiod=5)
>>> out.shape
(50, 3)
"""
arr = np.asarray(data, dtype=np.float64)
if arr.ndim == 1:
return fn(arr, **kwargs)
if arr.ndim != 2:
raise ValueError(f"batch_apply expects 1-D or 2-D input; got {arr.ndim}-D")
n_samples, n_series = arr.shape
result = np.empty((n_samples, n_series), dtype=np.float64)
for j in range(n_series):
result[:, j] = fn(arr[:, j], **kwargs)
return result
def batch_sma(
data: ArrayLike,
timeperiod: int = 30,
parallel: bool = True,
) -> np.ndarray:
"""Simple Moving Average on every column of *data*.
For 2-D inputs uses a Rust-side column loop (single GIL release).
When *parallel* is ``True`` (default), columns are processed in parallel
via Rayon across all available CPU cores.
1-D input is passed directly to the single-series SMA.
Parameters
----------
data : array-like, shape (n_samples,) or (n_samples, n_series)
timeperiod : int, default 30
parallel : bool, default True
Enable multi-threaded parallel column processing via Rayon.
Set to ``False`` for small inputs where thread overhead dominates.
Returns
-------
numpy.ndarray — same shape as *data*.
Examples
--------
>>> import numpy as np
>>> from ferro_ta.data.batch import batch_sma
>>> data = np.arange(1.0, 101.0).reshape(100, 1).repeat(3, axis=1)
>>> out = batch_sma(data, timeperiod=10)
>>> out.shape
(100, 3)
"""
arr = np.ascontiguousarray(data, dtype=np.float64)
if arr.ndim == 1:
return SMA(arr, timeperiod=timeperiod)
if arr.ndim != 2:
raise ValueError(f"batch_sma expects 1-D or 2-D input; got {arr.ndim}-D")
return np.asarray(_rust_batch_sma(arr, timeperiod, parallel))
def batch_ema(
data: ArrayLike,
timeperiod: int = 30,
parallel: bool = True,
) -> np.ndarray:
"""Exponential Moving Average on every column of *data*.
For 2-D inputs uses a Rust-side column loop (single GIL release).
When *parallel* is ``True`` (default), columns are processed in parallel
via Rayon across all available CPU cores.
Parameters
----------
data : array-like, shape (n_samples,) or (n_samples, n_series)
timeperiod : int, default 30
parallel : bool, default True
Enable multi-threaded parallel column processing via Rayon.
Returns
-------
numpy.ndarray — same shape as *data*.
"""
arr = np.ascontiguousarray(data, dtype=np.float64)
if arr.ndim == 1:
return EMA(arr, timeperiod=timeperiod)
if arr.ndim != 2:
raise ValueError(f"batch_ema expects 1-D or 2-D input; got {arr.ndim}-D")
return np.asarray(_rust_batch_ema(arr, timeperiod, parallel))
def batch_rsi(
data: ArrayLike,
timeperiod: int = 14,
parallel: bool = True,
) -> np.ndarray:
"""Relative Strength Index on every column of *data*.
For 2-D inputs uses a Rust-side column loop (single GIL release).
When *parallel* is ``True`` (default), columns are processed in parallel
via Rayon across all available CPU cores.
Parameters
----------
data : array-like, shape (n_samples,) or (n_samples, n_series)
timeperiod : int, default 14
parallel : bool, default True
Enable multi-threaded parallel column processing via Rayon.
Returns
-------
numpy.ndarray — same shape as *data*. Values in [0, 100].
"""
arr = np.ascontiguousarray(data, dtype=np.float64)
if arr.ndim == 1:
return RSI(arr, timeperiod=timeperiod)
if arr.ndim != 2:
raise ValueError(f"batch_rsi expects 1-D or 2-D input; got {arr.ndim}-D")
return np.asarray(_rust_batch_rsi(arr, timeperiod, parallel))
def batch_atr(
high: ArrayLike,
low: ArrayLike,
close: ArrayLike,
timeperiod: int = 14,
parallel: bool = True,
) -> np.ndarray:
h = np.ascontiguousarray(high, dtype=np.float64)
low_arr = np.ascontiguousarray(low, dtype=np.float64)
c = np.ascontiguousarray(close, dtype=np.float64)
return np.asarray(_rust_batch_atr(h, low_arr, c, timeperiod, parallel))
def batch_stoch(
high: ArrayLike,
low: ArrayLike,
close: ArrayLike,
fastk_period: int = 5,
slowk_period: int = 3,
slowd_period: int = 3,
parallel: bool = True,
) -> tuple[np.ndarray, np.ndarray]:
h = np.ascontiguousarray(high, dtype=np.float64)
low_arr = np.ascontiguousarray(low, dtype=np.float64)
c = np.ascontiguousarray(close, dtype=np.float64)
k, d = _rust_batch_stoch(
h, low_arr, c, fastk_period, slowk_period, slowd_period, parallel
)
return np.asarray(k), np.asarray(d)
def batch_adx(
high: ArrayLike,
low: ArrayLike,
close: ArrayLike,
timeperiod: int = 14,
parallel: bool = True,
) -> np.ndarray:
h = np.ascontiguousarray(high, dtype=np.float64)
low_arr = np.ascontiguousarray(low, dtype=np.float64)
c = np.ascontiguousarray(close, dtype=np.float64)
return np.asarray(_rust_batch_adx(h, low_arr, c, timeperiod, parallel))