diff --git a/README.md b/README.md index 18b8c137..d7ad59ab 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ $ pip install pandas_ta[full] ## Development Version -The _development_ version, _0.4.14b_, includes _numerous_ bug fixes, speed improvements and better documentation since release, _0.3.14b_. +The _development_ version, _0.4.15b_, includes _numerous_ bug fixes, speed improvements and better documentation since release, _0.3.14b_. ```sh $ pip install -U git+https://github.com/twopirllc/pandas-ta.git@development @@ -1011,10 +1011,10 @@ Back to [Contents](#contents) - _Trendflex_: **trendflex** - **reflex** companion - _Trend Signals_: **tsignals** -- _TTM Trend_: **ttm_trend** - _Vertical Horizontal Filter_: **vhf** - _Vortex_: **vortex** - _Cross Signals_: **xsignals** +- _Zigzag_: **zigzag**
diff --git a/pandas_ta/core.py b/pandas_ta/core.py index e716f8f7..059b1a90 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -1563,13 +1563,6 @@ def tsignals(self, trend=None, asbool=None, trend_reset=None, trend_offset=None, result = tsignals(trend, asbool=asbool, trend_offset=trend_offset, trend_reset=trend_reset, offset=offset, **kwargs) return self._post_process(result, **kwargs) - def ttm_trend(self, length=None, offset=None, **kwargs: DictLike): - high = self._get_column(kwargs.pop("high", "high")) - low = self._get_column(kwargs.pop("low", "low")) - close = self._get_column(kwargs.pop("close", "close")) - result = ttm_trend(high=high, low=low, close=close, length=length, offset=offset, **kwargs) - return self._post_process(result, **kwargs) - def vhf(self, length=None, drift=None, offset=None, **kwargs: DictLike): close = self._get_column(kwargs.pop("close", "close")) result = vhf(close=close, length=length, drift=drift, offset=offset, **kwargs) @@ -1590,16 +1583,17 @@ def xsignals(self, signal=None, xa=None, xb=None, above=None, long=None, asbool= trend_offset=trend_offset, trend_reset=trend_reset, offset=offset, **kwargs) return self._post_process(result, **kwargs) - def zigzag(self, close=None, pivot_leg=None, price_deviation=None, retrace=None, last_extreme=None, offset=None, **kwargs: DictLike): + def zigzag(self, close=None, legs=None, deviation=None, retrace=None, last_extreme=None, offset=None, **kwargs: DictLike): high = self._get_column(kwargs.pop("high", "high")) low = self._get_column(kwargs.pop("low", "low")) if close is not None: close = self._get_column(kwargs.pop("close", "close")) result = zigzag( high=high, low=low, close=close, - pivot_leg=pivot_leg, price_deviation=price_deviation, + legs=legs, deviation=deviation, retrace=retrace, last_extreme=last_extreme, - offset=offset, **kwargs) + offset=offset, **kwargs + ) return self._post_process(result, **kwargs) # Volatility diff --git a/pandas_ta/maps.py b/pandas_ta/maps.py index 1760e543..eb101cbe 100644 --- a/pandas_ta/maps.py +++ b/pandas_ta/maps.py @@ -74,9 +74,9 @@ # Trend "trend": [ "adx", "alphatrend", "amat", "aroon", "chop", "cksp", "decay", - "decreasing", "dpo", "ht_trendline", "increasing", "long_run", "psar", "qstick", - "rwi", "short_run", "trendflex", "tsignals", "ttm_trend", "vhf", - "vortex", "xsignals", "zigzag" + "decreasing", "dpo", "ht_trendline", "increasing", "long_run", + "psar", "qstick", "rwi", "short_run", "trendflex", "tsignals", + "vhf", "vortex", "xsignals", "zigzag" ], # Volatility "volatility": [ diff --git a/pandas_ta/trend/__init__.py b/pandas_ta/trend/__init__.py index 240c33b1..b001b1dc 100644 --- a/pandas_ta/trend/__init__.py +++ b/pandas_ta/trend/__init__.py @@ -17,7 +17,6 @@ from .short_run import short_run from .trendflex import trendflex from .tsignals import tsignals -from .ttm_trend import ttm_trend from .vhf import vhf from .vortex import vortex from .xsignals import xsignals @@ -42,7 +41,6 @@ "short_run", "trendflex", "tsignals", - "ttm_trend", "vhf", "vortex", "xsignals", diff --git a/pandas_ta/trend/zigzag.py b/pandas_ta/trend/zigzag.py index f7e9b1cf..d01761f3 100644 --- a/pandas_ta/trend/zigzag.py +++ b/pandas_ta/trend/zigzag.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from numpy import isnan, nan, zeros, zeros_like, floor +from numpy import floor, isnan, nan, zeros, zeros_like from numba import njit from pandas import Series, DataFrame from pandas_ta._typing import DictLike, Int, IntFloat @@ -11,16 +11,17 @@ ) + @njit(cache=True) -def nb_rolling_hl(np_high, np_low, window): - extremums = 0 +def nb_rolling_hl(np_high, np_low, window_size): m = np_high.size - idx, value = zeros(m), zeros(m) - kind = zeros(m) # High Swing = 1, Low Swing = -1 + idx = zeros(m) + swing = zeros(m) # where a high = 1 and low = -1 + value = zeros(m) - left = int(floor(window / 2)) + extremums = 0 + left = int(floor(window_size / 2)) right = left + 1 - # sample_array = [*[left-window], *[center], *[right-window]] for i in range(left, m - right): low_center = np_low[i] @@ -30,95 +31,97 @@ def nb_rolling_hl(np_high, np_low, window): if (low_center <= low_window).all(): idx[extremums] = i - kind[extremums] = -1 + swing[extremums] = -1 value[extremums] = low_center extremums += 1 if (high_center >= high_window).all(): idx[extremums] = i - kind[extremums] = 1 + swing[extremums] = 1 value[extremums] = high_center extremums += 1 - return idx[:extremums], kind[:extremums], value[:extremums] + return idx[:extremums], swing[:extremums], value[:extremums] @njit(cache=True) -def nb_find_zigzags(idx, kind, value, deviation): - rolling_len, zigzags = idx.size, 0 - - idx = zeros_like(idx) - zigzag_types = zeros_like(kind) - zigzag_values = zeros_like(value) - zigzag_dev = zeros(rolling_len) - - idx[zigzags] = idx[-1] - zigzag_types[zigzags] = kind[-1] - zigzag_values[zigzags] = value[-1] - zigzag_dev[zigzags] = 0 - - for i in range(rolling_len - 2, -1, -1): +def nb_find_zigzags(idx, swing, value, deviation): + zz_idx = zeros_like(idx) + zz_swing = zeros_like(swing) + zz_value = zeros_like(value) + zz_dev = zeros_like(idx) + + zigzags = 0 + zz_idx[zigzags] = idx[-1] + zz_swing[zigzags] = swing[-1] + zz_value[zigzags] = value[-1] + zz_dev[zigzags] = 0 + + m = idx.size + for i in range(m - 2, -1, -1): # last point in zigzag is bottom - if zigzag_types[zigzags] == -1: - if kind[i] == -1: - if zigzag_values[zigzags] > value[i] and zigzags > 1: - current_deviation = (zigzag_values[zigzags - 1] - value[i]) / value[i] - idx[zigzags] = idx[i] - zigzag_types[zigzags] = kind[i] - zigzag_values[zigzags] = value[i] - zigzag_dev[zigzags - 1] = 100 * current_deviation + if zz_swing[zigzags] == -1: + if swing[i] == -1: + if zz_value[zigzags] > value[i] and zigzags > 1: + current_dev = (zz_value[zigzags - 1] - value[i]) / value[i] + zz_idx[zigzags] = idx[i] + zz_swing[zigzags] = swing[i] + zz_value[zigzags] = value[i] + zz_dev[zigzags - 1] = 100 * current_dev else: - current_deviation = (value[i] - zigzag_values[zigzags]) / value[i] - if current_deviation > deviation / 100: - if idx[zigzags] == idx[i]: + current_dev = (value[i] - zz_value[zigzags]) / value[i] + if current_dev > 0.01 * deviation: + if zz_idx[zigzags] == idx[i]: continue zigzags += 1 - idx[zigzags] = idx[i] - zigzag_types[zigzags] = kind[i] - zigzag_values[zigzags] = value[i] - zigzag_dev[zigzags - 1] = 100 * current_deviation + zz_idx[zigzags] = idx[i] + zz_swing[zigzags] = swing[i] + zz_value[zigzags] = value[i] + zz_dev[zigzags - 1] = 100 * current_dev # last point in zigzag is peak else: - if kind[i] == 1: - if zigzag_values[zigzags] < value[i] and zigzags > 1: - current_deviation = (value[i] - zigzag_values[zigzags - 1]) / value[i] - idx[zigzags] = idx[i] - zigzag_types[zigzags] = kind[i] - zigzag_values[zigzags] = value[i] - zigzag_dev[zigzags - 1] = 100 * current_deviation + if swing[i] == 1: + if zz_value[zigzags] < value[i] and zigzags > 1: + current_dev = (value[i] - zz_value[zigzags - 1]) / value[i] + zz_idx[zigzags] = idx[i] + zz_swing[zigzags] = swing[i] + zz_value[zigzags] = value[i] + zz_dev[zigzags - 1] = 100 * current_dev else: - current_deviation = (zigzag_values[zigzags] - value[i]) / value[i] - if current_deviation > deviation / 100: - if idx[zigzags] == idx[i]: + current_dev = (zz_value[zigzags] - value[i]) / value[i] + if current_dev > 0.01 * deviation: + if zz_idx[zigzags] == idx[i]: continue zigzags += 1 - idx[zigzags] = idx[i] - zigzag_types[zigzags] = kind[i] - zigzag_values[zigzags] = value[i] - zigzag_dev[zigzags - 1] = 100 * current_deviation + zz_idx[zigzags] = idx[i] + zz_swing[zigzags] = swing[i] + zz_value[zigzags] = value[i] + zz_dev[zigzags - 1] = 100 * current_dev - return idx[:zigzags + 1], zigzag_types[:zigzags + 1], \ - zigzag_values[:zigzags + 1], zigzag_dev[:zigzags + 1] + _n = zigzags + 1 + return zz_idx[:_n], zz_swing[:_n], zz_value[:_n], zz_dev[:_n] @njit(cache=True) -def nb_map_zigzag(zigzag_idx, zigzag_types, zigzag_values, zigzag_dev, candles_num): - _values = zeros(candles_num) - _types = zeros(candles_num) - _dev = zeros(candles_num) +def nb_map_zigzag(idx, swing, value, deviation, n): + swing_map = zeros(n) + value_map = zeros(n) + dev_map = zeros(n) - for i, index in enumerate(zigzag_idx): - _values[int(index)] = zigzag_values[i] - _types[int(index)] = zigzag_types[i] - _dev[int(index)] = zigzag_dev[i] + for j, i in enumerate(idx): + i = int(i) + swing_map[i] = swing[j] + value_map[i] = value[j] + dev_map[i] = deviation[j] - for i in range(candles_num): - if _types[i] == 0: - _values[i] = nan - _types[i] = nan - _dev[i] = nan - return _types, _values, _dev + for i in range(n): + if swing_map[i] == 0: + swing_map[i] = nan + value_map[i] = nan + dev_map[i] = nan + + return swing_map, value_map, dev_map @@ -128,7 +131,7 @@ def zigzag( retrace: bool = None, last_extreme: bool = None, offset: Int = None, **kwargs: DictLike ): - """Zigzag (ZIGZAG) + """ Zigzag (ZIGZAG) Zigzag attempts to filter out smaller price movments while highlighting trend direction. It does not predict future trends, but it does identify @@ -156,22 +159,24 @@ def zigzag( Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method Returns: pd.DataFrame: swing, and swing_type (high or low). """ # Validate - _length = 0 - legs = _length = v_pos_default(legs, 10) - high = v_series(high, _length + 1) - low = v_series(low, _length + 1) + legs = v_pos_default(legs, 10) + _length = legs + 1 + # print(f"\n{legs=} {_length=}") + high = v_series(high, _length) + low = v_series(low, _length) if high is None or low is None: return if close is not None: - close = v_series(close, _length + 1) - np_close = close.to_numpy() + close = v_series(close,_length) + np_close = close.values if close is None: return @@ -182,36 +187,38 @@ def zigzag( # Calculation np_high, np_low = high.to_numpy(), low.to_numpy() + hli, hls, hlv = nb_rolling_hl(np_high, np_low, legs) + # print(f"{len(high)=} {np_high.size=}") + # print(f"\nhli[{hli.size}]: {hli}\nhls[{hls.size}]: {hls}\nhlv[{hlv.size}]: {hlv}\n") + + zzi, zzs, zzv, zzd = nb_find_zigzags(hli, hls, hlv, deviation) + # print(f"\nzzi[{zzi.size}]: {zzi}\nzzs[{zzs.size}]: {zzs}\nzzv[{zzv.size}]: {zzv}\nzzd[{zzd.size}]: {zzd}\n") - _rollings_idx, _rollings_types, _rollings_values = nb_rolling_hl(np_high, np_low, legs) - _zigzags_idx, _zigzags_types, _zigzags_values, _zigzags_dev = \ - nb_find_zigzags(_rollings_idx, _rollings_types, _rollings_values, deviation=deviation) - _types, _values, _dev = \ - nb_map_zigzag(_zigzags_idx, _zigzags_types, _zigzags_values, _zigzags_dev, len(high)) + zz_swing, zz_value, zz_dev = nb_map_zigzag(zzi, zzs, zzv, zzd, np_high.size) + # print(f"\nzz_swing[{zz_swing.size}]: {zz_swing}\nzz_value[{zz_value.size}]: {zz_value}\nzz_dev[{zz_dev.size}]: {zz_dev}\n") # Offset if offset != 0: - _types = _types.shift(offset) - _values = _values.shift(offset) - _dev = _dev.shift(offset) + zz_swing = zz_swing.shift(offset) + zz_value = zz_value.shift(offset) + zz_dev = zz_dev.shift(offset) # Fill if "fillna" in kwargs: - _types.fillna(kwargs["fillna"], inplace=True) - _values.fillna(kwargs["fillna"], inplace=True) - _dev.fillna(kwargs["fillna"], inplace=True) + zz_swing.fillna(kwargs["fillna"], inplace=True) + zz_value.fillna(kwargs["fillna"], inplace=True) + zz_dev.fillna(kwargs["fillna"], inplace=True) if "fill_method" in kwargs: - _types.fillna(method=kwargs["fill_method"], inplace=True) - _values.fillna(method=kwargs["fill_method"], inplace=True) - _dev.fillna(method=kwargs["fill_method"], inplace=True) + zz_swing.fillna(method=kwargs["fill_method"], inplace=True) + zz_value.fillna(method=kwargs["fill_method"], inplace=True) + zz_dev.fillna(method=kwargs["fill_method"], inplace=True) - # Name and Category _props = f"_{deviation}%_{legs}" data = { - f"ZIGZAGt{_props}": _types, - f"ZIGZAGv{_props}": _values, - f"ZIGZAGd{_props}": _dev, + f"ZIGZAGs{_props}": zz_swing, + f"ZIGZAGv{_props}": zz_value, + f"ZIGZAGd{_props}": zz_dev, } df = DataFrame(data, index=high.index) df.name = f"ZIGZAG{_props}" diff --git a/setup.py b/setup.py index e69cad48..acb9473b 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ "pandas_ta.volatility", "pandas_ta.volume" ], - version=".".join(("0", "4", "14b")), + version=".".join(("0", "4", "15b")), description=long_description, long_description=long_description, author="Kevin Johnson", diff --git a/tests/test_indicator_trend.py b/tests/test_indicator_trend.py index 30da9f8e..505bb7a4 100644 --- a/tests/test_indicator_trend.py +++ b/tests/test_indicator_trend.py @@ -265,12 +265,6 @@ def test_trendflex(df): assert result.name == "TRENDFLEX_20_20_0.04" -def test_ttm_trend(df): - result = ta.ttm_trend(df.high, df.low, df.close) - assert isinstance(result, DataFrame) - assert result.name == "TTMTREND_6" - - def test_vhf(df): result = ta.vhf(df.high, df.low, df.close) assert isinstance(result, Series) @@ -283,6 +277,30 @@ def test_vortex(df): assert result.name == "VTX_14" +def test_zigzag(df): + result = ta.zigzag(df.high, df.low) + assert isinstance(result, DataFrame) + assert result.name == "ZIGZAG_5.0%_10" + + result = ta.zigzag(df.high, df.low, df.close) + assert isinstance(result, DataFrame) + assert result.name == "ZIGZAG_5.0%_10" + + notna = result.iloc[:,0].notna() + high_pivotsdf = result[notna & (result["ZIGZAGs_5.0%_10"]==1)] + assert isinstance(high_pivotsdf, DataFrame) + assert high_pivotsdf.shape[0] == 1 + + low_pivotsdf = result[notna & (result["ZIGZAGs_5.0%_10"]==-1)] + assert isinstance(low_pivotsdf, DataFrame) + assert low_pivotsdf.shape[0] == 1 + + all_pivotsdf = result[notna] + assert isinstance(all_pivotsdf, DataFrame) + assert all_pivotsdf.shape[0] == low_pivotsdf.shape[0] + high_pivotsdf.shape[0] + + + # DataFrame Extension Tests def test_ext_adx(df): df.ta.adx(append=True) @@ -396,11 +414,6 @@ def test_ext_trendflex(df): assert df.columns[-1] == "TRENDFLEX_20_20_0.04" -def test_ext_ttm_trend(df): - df.ta.ttm_trend(append=True) - assert df.columns[-1] == "TTM_TRND_6" - - def test_ext_vhf(df): df.ta.vhf(append=True) assert df.columns[-1] == "VHF_28" @@ -409,3 +422,8 @@ def test_ext_vhf(df): def test_ext_vortex(df): df.ta.vortex(append=True) assert list(df.columns[-2:]) == ["VTXP_14", "VTXM_14"] + + +def test_ext_zigzag(df): + df.ta.zigzag(append=True) + assert list(df.columns[-3:]) == ["ZIGZAGs_5.0%_10", "ZIGZAGv_5.0%_10", "ZIGZAGd_5.0%_10"] diff --git a/tests/test_studies.py b/tests/test_studies.py index c40fb192..3d9d5f17 100644 --- a/tests/test_studies.py +++ b/tests/test_studies.py @@ -9,7 +9,7 @@ [pytest.param(ta.CommonStudy, id="common"), pytest.param(ta.AllStudy, id="all")] # +/- when adding/removing indicators -ALL_COLUMNS = 323 +ALL_COLUMNS = 325 def test_all_study_props(all_study): @@ -32,7 +32,7 @@ def test_common_study_props(common_study): @pytest.mark.parametrize("category,columns", [ ("candles", 70), ("cycles", 2), ("momentum", 78), ("overlap", 56), - ("performance", 2), ("statistics", 16), ("transform", 5), ("trend", 31), + ("performance", 2), ("statistics", 16), ("transform", 5), ("trend", 33), ("volatility", 36), ("volume", 27), pytest.param(ta.AllStudy, ALL_COLUMNS, id=f"all-{ALL_COLUMNS}"), pytest.param(ta.CommonStudy, 5, id="common-5"),