From a0808f2ed9a3faaa4dd65e3cf6dc4a7dd5b9f642 Mon Sep 17 00:00:00 2001 From: whning Date: Mon, 29 Jun 2026 01:20:40 +0800 Subject: [PATCH 1/2] Reject MACD signalperiod=1 for look-ahead affected TA-Lib paths --- talib/__init__.py | 22 ++++++++++++++++++++++ talib/abstract.py | 22 ++++++++++++++++++++-- tests/test_abstract.py | 10 ++++++++++ tests/test_func.py | 13 +++++++++++++ tests/test_stream.py | 11 +++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/talib/__init__.py b/talib/__init__.py index c2df327d1..790f4bd21 100644 --- a/talib/__init__.py +++ b/talib/__init__.py @@ -106,6 +106,24 @@ def wrapper(*args, **kwds): _wrapper = lambda x: x +def _validate_macd_signalperiod(func_name, kwargs): + signalperiod = kwargs.get('signalperiod') + if signalperiod == 1: + raise ValueError( + f"signalperiod=1 is not supported for {func_name} because the underlying TA-Lib " + "implementation can produce look-ahead affected results; use signalperiod >= 2 instead." + ) + + +def _macd_wrapper(func_name, func): + @wraps(func) + def wrapper(*args, **kwds): + _validate_macd_signalperiod(func_name, kwds) + return func(*args, **kwds) + + return wrapper + + from ._ta_lib import ( _ta_initialize, _ta_shutdown, MA_Type, __ta_version__, _ta_set_unstable_period as set_unstable_period, @@ -122,6 +140,8 @@ def wrapper(*args, **kwds): func = __import__("_ta_lib", globals(), locals(), __TA_FUNCTION_NAMES__, level=1) for func_name in __TA_FUNCTION_NAMES__: wrapped_func = _wrapper(getattr(func, func_name)) + if func_name in ('MACD', 'MACDFIX'): + wrapped_func = _macd_wrapper(func_name, wrapped_func) setattr(func, func_name, wrapped_func) globals()[func_name] = wrapped_func @@ -129,6 +149,8 @@ def wrapper(*args, **kwds): 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)) + if func_name in ('MACD', 'MACDFIX'): + wrapped_func = _macd_wrapper(func_name, wrapped_func) setattr(stream, func_name, wrapped_func) globals()[stream_func_name] = wrapped_func diff --git a/talib/abstract.py b/talib/abstract.py index 2ee738ca0..d3ee37154 100644 --- a/talib/abstract.py +++ b/talib/abstract.py @@ -10,13 +10,31 @@ } +class _FunctionProxy: + def __init__(self, func_name, func_obj): + self._func_name = func_name + self._func_obj = func_obj + + def __call__(self, *args, **kwargs): + if self._func_name in ('MACD', 'MACDFIX') and kwargs.get('signalperiod') == 1: + raise ValueError( + f"signalperiod=1 is not supported for {self._func_name} because the underlying TA-Lib " + "implementation can produce look-ahead affected results; use signalperiod >= 2 instead." + ) + return self._func_obj(*args, **kwargs) + + def __getattr__(self, item): + return getattr(self._func_obj, item) + + def Function(function_name, *args, **kwargs): func_name = function_name.upper() if func_name not in _func_obj_mapping: raise Exception('%s not supported by TA-LIB.' % func_name) - return _Function( - func_name, _func_obj_mapping[func_name], *args, **kwargs + return _FunctionProxy( + func_name, + _Function(func_name, _func_obj_mapping[func_name], *args, **kwargs) ) diff --git a/tests/test_abstract.py b/tests/test_abstract.py index c7440e58c..03d5c35b4 100644 --- a/tests/test_abstract.py +++ b/tests/test_abstract.py @@ -25,6 +25,16 @@ def test_pararmeters(): assert all(type(v) == int for k, v in parameters.items()) +def test_macd_signalperiod_one_rejected(): + values = np.linspace(1.0, 100.0, 100, dtype=float) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACD"): + abstract.MACD(values, signalperiod=1) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACDFIX"): + abstract.MACDFIX(values, signalperiod=1) + + def test_pandas(ford_2012): import pandas input_df = pandas.DataFrame(ford_2012) diff --git a/tests/test_func.py b/tests/test_func.py index cb1e36ffa..fc1e070f3 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -63,6 +63,19 @@ def test_unstable_period(): talib.set_unstable_period('EMA', 0) +def test_macd_signalperiod_one_rejected(): + values = np.linspace(1.0, 100.0, 100, dtype=float) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACD"): + talib.MACD(values, signalperiod=1) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACD"): + func.MACD(values, signalperiod=1) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACDFIX"): + talib.MACDFIX(values, signalperiod=1) + + def test_compatibility(): a = np.arange(10, dtype=float) talib.set_compatibility(0) diff --git a/tests/test_stream.py b/tests/test_stream.py index afb99edcf..dd5ff7afb 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,5 +1,6 @@ import numpy as np import pandas as pd +import pytest from talib import stream @@ -64,3 +65,13 @@ def test_MAXINDEX(): a = np.array([1., 2, 3, 4, 5, 6, 7, 8, 7, 7, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 15]) r = stream.MAXINDEX(a, 10) assert r == 21 + + +def test_stream_macd_signalperiod_one_rejected(): + values = np.linspace(1.0, 100.0, 100, dtype=float) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACD"): + stream.MACD(values, signalperiod=1) + + with pytest.raises(ValueError, match="signalperiod=1 is not supported for MACDFIX"): + stream.MACDFIX(values, signalperiod=1) From 5c3921b0a9ca45972734932edbdd7d450a392f37 Mon Sep 17 00:00:00 2001 From: whn <142425816+Whning0513@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:56:02 +0800 Subject: [PATCH 2/2] Fix abstract proxy setters for existing Function state --- talib/abstract.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/talib/abstract.py b/talib/abstract.py index d3ee37154..cb90df9f2 100644 --- a/talib/abstract.py +++ b/talib/abstract.py @@ -12,8 +12,8 @@ class _FunctionProxy: def __init__(self, func_name, func_obj): - self._func_name = func_name - self._func_obj = func_obj + object.__setattr__(self, '_func_name', func_name) + object.__setattr__(self, '_func_obj', func_obj) def __call__(self, *args, **kwargs): if self._func_name in ('MACD', 'MACDFIX') and kwargs.get('signalperiod') == 1: @@ -26,6 +26,12 @@ def __call__(self, *args, **kwargs): def __getattr__(self, item): return getattr(self._func_obj, item) + def __setattr__(self, key, value): + if key in {'_func_name', '_func_obj'}: + object.__setattr__(self, key, value) + return + setattr(self._func_obj, key, value) + def Function(function_name, *args, **kwargs): func_name = function_name.upper()