436954138f
Update version numbers across Rust, Python, and documentation files to 1.1.0. Enhance the .gitignore to include macOS dSYM files and plans directory. Introduce new dependencies in the Rust core library and update the README to reflect recent performance benchmarks and backtesting engine capabilities. Add new artifacts to the benchmarks manifest and improve documentation for the backtesting engine API.
499 lines
13 KiB
Python
499 lines
13 KiB
Python
"""
|
||
Extended Indicators — Popular indicators not in the TA-Lib standard set.
|
||
|
||
All indicator logic is implemented in Rust (PyO3) for maximum performance.
|
||
This module provides the public Python API with:
|
||
- Input validation
|
||
- ``_to_f64`` conversion
|
||
- pandas/polars-compatible return values (numpy arrays)
|
||
|
||
Functions
|
||
---------
|
||
VWAP — Volume Weighted Average Price (cumulative or rolling)
|
||
SUPERTREND — ATR-based trend-following signal
|
||
ICHIMOKU — Ichimoku Cloud
|
||
DONCHIAN — Donchian Channels
|
||
PIVOT_POINTS — Classic / Fibonacci / Camarilla pivot levels
|
||
KELTNER_CHANNELS — EMA ± ATR bands
|
||
HULL_MA — Hull Moving Average (WMA-based)
|
||
CHANDELIER_EXIT — ATR-based stop-loss / exit levels
|
||
VWMA — Volume Weighted Moving Average
|
||
CHOPPINESS_INDEX — Market choppiness / trending strength index
|
||
|
||
Rust backend
|
||
------------
|
||
All computations delegate to Rust functions in the ``_ferro_ta`` extension::
|
||
|
||
from ferro_ta._ferro_ta import supertrend, donchian, vwap, ...
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import numpy as np
|
||
from numpy.typing import ArrayLike
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Import Rust implementations
|
||
# ---------------------------------------------------------------------------
|
||
from ferro_ta._ferro_ta import (
|
||
chandelier_exit as _rust_chandelier_exit,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
choppiness_index as _rust_choppiness_index,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
donchian as _rust_donchian,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
hull_ma as _rust_hull_ma,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
ichimoku as _rust_ichimoku,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
keltner_channels as _rust_keltner_channels,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
pivot_points as _rust_pivot_points,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
supertrend as _rust_supertrend,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
vwap as _rust_vwap,
|
||
)
|
||
from ferro_ta._ferro_ta import (
|
||
vwma as _rust_vwma,
|
||
)
|
||
from ferro_ta._utils import _to_f64
|
||
from ferro_ta.core.exceptions import FerroTAValueError, _normalize_rust_error
|
||
|
||
|
||
def VWAP(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
volume: ArrayLike,
|
||
timeperiod: int = 0,
|
||
) -> np.ndarray:
|
||
"""Volume Weighted Average Price.
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
Sequence of high prices.
|
||
low : array-like
|
||
Sequence of low prices.
|
||
close : array-like
|
||
Sequence of closing prices.
|
||
volume : array-like
|
||
Sequence of volumes.
|
||
timeperiod : int, optional
|
||
Rolling window length. ``0`` (default) computes a cumulative VWAP
|
||
from bar 0 (session VWAP). Any value ``>= 1`` uses a rolling window
|
||
of that length; the first ``timeperiod - 1`` values are ``NaN``.
|
||
|
||
Returns
|
||
-------
|
||
numpy.ndarray
|
||
Array of VWAP values.
|
||
|
||
Notes
|
||
-----
|
||
Typical price is used: ``(high + low + close) / 3``.
|
||
Implemented in Rust for maximum performance.
|
||
"""
|
||
if timeperiod < 0:
|
||
raise FerroTAValueError("timeperiod must be >= 0 for VWAP")
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
v = _to_f64(volume)
|
||
try:
|
||
return np.asarray(_rust_vwap(h, lo, c, v, timeperiod))
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
|
||
|
||
def SUPERTREND(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
timeperiod: int = 7,
|
||
multiplier: float = 3.0,
|
||
) -> tuple[np.ndarray, np.ndarray]:
|
||
"""Supertrend indicator.
|
||
|
||
An ATR-based trend-following indicator. Returns the Supertrend line and a
|
||
direction array.
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
Sequence of high prices.
|
||
low : array-like
|
||
Sequence of low prices.
|
||
close : array-like
|
||
Sequence of closing prices.
|
||
timeperiod : int, optional
|
||
ATR period (default 7).
|
||
multiplier : float, optional
|
||
ATR multiplier for band width (default 3.0).
|
||
|
||
Returns
|
||
-------
|
||
supertrend : numpy.ndarray
|
||
The Supertrend line values. ``NaN`` during the warmup period.
|
||
direction : numpy.ndarray
|
||
``1`` = uptrend (price above Supertrend), ``-1`` = downtrend.
|
||
``0`` during warmup.
|
||
|
||
Notes
|
||
-----
|
||
Implemented in Rust — the sequential band-adjustment loop that was
|
||
previously a Python bottleneck now runs at native speed.
|
||
|
||
Examples
|
||
--------
|
||
>>> import numpy as np
|
||
>>> from ferro_ta import SUPERTREND
|
||
>>> h = np.array([10.0, 11.0, 12.0, 11.0, 10.0, 9.0, 8.0, 9.0, 10.0, 11.0,
|
||
... 12.0, 13.0, 14.0, 13.0, 12.0])
|
||
>>> l = h - 1.0
|
||
>>> c = (h + l) / 2.0
|
||
>>> st, dir_ = SUPERTREND(h, l, c)
|
||
"""
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
try:
|
||
st, d = _rust_supertrend(h, lo, c, timeperiod, multiplier)
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
return np.asarray(st), np.asarray(d)
|
||
|
||
|
||
def ICHIMOKU(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
tenkan_period: int = 9,
|
||
kijun_period: int = 26,
|
||
senkou_b_period: int = 52,
|
||
displacement: int = 26,
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||
"""Ichimoku Cloud (Ichimoku Kinko Hyo).
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
low : array-like
|
||
close : array-like
|
||
tenkan_period : int, default 9
|
||
Conversion line (Tenkan-sen) period.
|
||
kijun_period : int, default 26
|
||
Base line (Kijun-sen) period.
|
||
senkou_b_period : int, default 52
|
||
Leading Span B period.
|
||
displacement : int, default 26
|
||
Displacement / cloud offset for Senkou A & B.
|
||
|
||
Returns
|
||
-------
|
||
tenkan, kijun, senkou_a, senkou_b, chikou : numpy.ndarray
|
||
Each is a 1-D float64 array of the same length as the inputs.
|
||
|
||
Notes
|
||
-----
|
||
Implemented in Rust with O(n) monotonic deque for all rolling windows.
|
||
"""
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
try:
|
||
t, k, sa, sb, ch = _rust_ichimoku(
|
||
h, lo, c, tenkan_period, kijun_period, senkou_b_period, displacement
|
||
)
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
return (
|
||
np.asarray(t),
|
||
np.asarray(k),
|
||
np.asarray(sa),
|
||
np.asarray(sb),
|
||
np.asarray(ch),
|
||
)
|
||
|
||
|
||
def DONCHIAN(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
timeperiod: int = 20,
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||
"""Donchian Channels — rolling highest high / lowest low.
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
low : array-like
|
||
timeperiod : int, default 20
|
||
|
||
Returns
|
||
-------
|
||
upper, middle, lower : numpy.ndarray
|
||
Rolling highest high, midpoint, and lowest low.
|
||
|
||
Notes
|
||
-----
|
||
Implemented in Rust with O(n) monotonic deque (no Python loop).
|
||
"""
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
try:
|
||
upper, middle, lower = _rust_donchian(h, lo, timeperiod)
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
return np.asarray(upper), np.asarray(middle), np.asarray(lower)
|
||
|
||
|
||
def PIVOT_POINTS(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
method: str = "classic",
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
||
"""Pivot Points — support / resistance levels.
|
||
|
||
Computes pivot points for each bar using the *previous bar's* H/L/C.
|
||
The first bar output is NaN.
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
low : array-like
|
||
close : array-like
|
||
method : {'classic', 'fibonacci', 'camarilla'}, default 'classic'
|
||
|
||
Returns
|
||
-------
|
||
pivot, r1, s1, r2, s2 : numpy.ndarray
|
||
|
||
Notes
|
||
-----
|
||
**Classic**: P=(H+L+C)/3; R1=2P−L; S1=2P−H; R2=P+(H−L); S2=P−(H−L)
|
||
|
||
**Fibonacci**: P=(H+L+C)/3; R1=P+0.382*(H−L); S1=P−0.382*(H−L);
|
||
R2=P+0.618*(H−L); S2=P−0.618*(H−L)
|
||
|
||
**Camarilla**: P=(H+L+C)/3; R1=C+1.1*(H−L)/12; S1=C−1.1*(H−L)/12;
|
||
R2=C+1.1*(H−L)/6; S2=C−1.1*(H−L)/6
|
||
"""
|
||
valid_methods = {"classic", "fibonacci", "camarilla"}
|
||
if method.lower() not in valid_methods:
|
||
raise FerroTAValueError(
|
||
f"Unknown pivot method '{method}'. Use 'classic', 'fibonacci', or 'camarilla'."
|
||
)
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
try:
|
||
pivot, r1, s1, r2, s2 = _rust_pivot_points(h, lo, c, method)
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
return (
|
||
np.asarray(pivot),
|
||
np.asarray(r1),
|
||
np.asarray(s1),
|
||
np.asarray(r2),
|
||
np.asarray(s2),
|
||
)
|
||
|
||
|
||
def KELTNER_CHANNELS(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
timeperiod: int = 20,
|
||
atr_period: int = 10,
|
||
multiplier: float = 2.0,
|
||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||
"""Keltner Channels — EMA ± (multiplier × ATR).
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
low : array-like
|
||
close : array-like
|
||
timeperiod : int, default 20
|
||
EMA period for the middle band.
|
||
atr_period : int, default 10
|
||
ATR period for band width.
|
||
multiplier : float, default 2.0
|
||
ATR multiplier.
|
||
|
||
Returns
|
||
-------
|
||
upper, middle, lower : numpy.ndarray
|
||
|
||
Notes
|
||
-----
|
||
Implemented in Rust — EMA and ATR computed inline without Python calls.
|
||
"""
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
try:
|
||
upper, middle, lower = _rust_keltner_channels(
|
||
h, lo, c, timeperiod, atr_period, multiplier
|
||
)
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
return np.asarray(upper), np.asarray(middle), np.asarray(lower)
|
||
|
||
|
||
def HULL_MA(
|
||
close: ArrayLike,
|
||
timeperiod: int = 16,
|
||
) -> np.ndarray:
|
||
"""Hull Moving Average (HMA).
|
||
|
||
A fast-responding moving average that reduces lag.
|
||
|
||
Parameters
|
||
----------
|
||
close : array-like
|
||
timeperiod : int, default 16
|
||
|
||
Returns
|
||
-------
|
||
numpy.ndarray
|
||
|
||
Notes
|
||
-----
|
||
Formula: ``HMA(n) = WMA(2 * WMA(n/2) - WMA(n), sqrt(n))``
|
||
|
||
Implemented in Rust — all WMA computations are in-process.
|
||
"""
|
||
c = _to_f64(close)
|
||
try:
|
||
return np.asarray(_rust_hull_ma(c, timeperiod))
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
|
||
|
||
def CHANDELIER_EXIT(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
timeperiod: int = 22,
|
||
multiplier: float = 3.0,
|
||
) -> tuple[np.ndarray, np.ndarray]:
|
||
"""Chandelier Exit — ATR-based trailing stop levels.
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
low : array-like
|
||
close : array-like
|
||
timeperiod : int, default 22
|
||
Lookback period for highest high / lowest low and ATR.
|
||
multiplier : float, default 3.0
|
||
ATR multiplier.
|
||
|
||
Returns
|
||
-------
|
||
long_exit, short_exit : numpy.ndarray
|
||
|
||
Notes
|
||
-----
|
||
Implemented in Rust with O(n) monotonic deque for rolling max/min.
|
||
"""
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
try:
|
||
long_exit, short_exit = _rust_chandelier_exit(h, lo, c, timeperiod, multiplier)
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
return np.asarray(long_exit), np.asarray(short_exit)
|
||
|
||
|
||
def VWMA(
|
||
close: ArrayLike,
|
||
volume: ArrayLike,
|
||
timeperiod: int = 20,
|
||
) -> np.ndarray:
|
||
"""Volume Weighted Moving Average.
|
||
|
||
Parameters
|
||
----------
|
||
close : array-like
|
||
volume : array-like
|
||
timeperiod : int, default 20
|
||
|
||
Returns
|
||
-------
|
||
numpy.ndarray
|
||
|
||
Notes
|
||
-----
|
||
``VWMA = sum(close * volume, n) / sum(volume, n)``
|
||
Implemented in Rust with O(n) prefix-sum approach.
|
||
"""
|
||
c = _to_f64(close)
|
||
v = _to_f64(volume)
|
||
try:
|
||
return np.asarray(_rust_vwma(c, v, timeperiod))
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
|
||
|
||
def CHOPPINESS_INDEX(
|
||
high: ArrayLike,
|
||
low: ArrayLike,
|
||
close: ArrayLike,
|
||
timeperiod: int = 14,
|
||
) -> np.ndarray:
|
||
"""Choppiness Index — measures market choppiness (range-bound vs trending).
|
||
|
||
Parameters
|
||
----------
|
||
high : array-like
|
||
low : array-like
|
||
close : array-like
|
||
timeperiod : int, default 14
|
||
|
||
Returns
|
||
-------
|
||
numpy.ndarray
|
||
Values in ``[0, 100]``. Values near 100 indicate choppy/range-bound
|
||
markets; values near 0 indicate strong trends.
|
||
|
||
Notes
|
||
-----
|
||
``CI = 100 * log10(sum(ATR(1), n) / (highest_high − lowest_low)) / log10(n)``
|
||
|
||
Implemented in Rust with O(n) monotonic deques (no Python loop).
|
||
"""
|
||
h = _to_f64(high)
|
||
lo = _to_f64(low)
|
||
c = _to_f64(close)
|
||
try:
|
||
return np.asarray(_rust_choppiness_index(h, lo, c, timeperiod))
|
||
except ValueError as e:
|
||
_normalize_rust_error(e)
|
||
|
||
|
||
__all__ = [
|
||
"VWAP",
|
||
"SUPERTREND",
|
||
"ICHIMOKU",
|
||
"DONCHIAN",
|
||
"PIVOT_POINTS",
|
||
"KELTNER_CHANNELS",
|
||
"HULL_MA",
|
||
"CHANDELIER_EXIT",
|
||
"VWMA",
|
||
"CHOPPINESS_INDEX",
|
||
]
|