diff --git a/README.md b/README.md index 2e109d5..8d63436 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ _Pandas Technical Analysis_ (**Pandas TA**) is an easy to use library that lever * [Overlap](#overlap-36) * [Performance](#performance-3) * [Statistics](#statistics-11) + * [Transform](#transform-3) * [Trend](#trend-19) * [Utility](#utility-5) * [Volatility](#volatility-14) @@ -81,11 +82,11 @@ _Pandas Technical Analysis_ (**Pandas TA**) is an easy to use library that lever # **Features** -* Over 140 indicators and utility functions. +* Over 140+ indicators and utility functions. * **TA Lib** indicators (```pip install ta-lib```). * TA Lib's 63 Chart Patterns * Python Indicators are tightly correlated with the _de facto_ [TA Lib](https://github.com/mrjbq7/ta-lib). - * TA Lib computations are by default **enabled**. They can be disabled disabled per indicator by using the argument ```talib=False```. + * TA Lib computations are by default **enabled**. They can be disabled per indicator by using the argument ```talib=False```. * For example to disable TA Lib calculation for **stdev**: ```ta.stdev(df["close"], length=30, talib=False)```. * **Stochastic Sample Realizations** with the [stochastic](https://github.com/crflynn/stochastic) package (```pip install stochastic```). See the [Stochastic Samples](#stochastic-samples) section below. * **External Custom Indicators Directory** independent of the builtin Pandas TA indicators. For more information, see ```import_dir``` documentation under ```/pandas_ta/custom.py```. @@ -298,7 +299,7 @@ df.ta.study(MyStudy, **kwargs)
-The _Study_ Class is a simple way to name and group your favorite TA Indicators by using a _Data Class_. **Pandas TA** comes with two prebuilt basic Studies to help you get started: __AllStudy__ and __CommonStudy__. A _Study_ can be as simple as the __CommonStudy__ or as complex as needed using Composition/Chaining. +The _Study_ Class is a simple way to name and group your favorite TA Indicators by using a _Data Class_. **Pandas TA** comes with two prebuilt basic Studies to help you get started: __AllStudy__ and __CommonStudy__. A _Study_ can be as simple as the __CommonStudy__ or as complex as needed using Composition/Chaining. * When using the _study_ method, **all** indicators will be automatically appended to the DataFrame ```df```. * You are using a Chained Study when you have the output of one indicator as input into one or more indicators in the same _Study_. @@ -856,6 +857,14 @@ Use parameter: cumulative=**True** for cumulative results.
+### **Transform** (3) + +* _Cube Transform_: **cube** +* _Inverse Fisher Transform_: **ifisher** +* _ReMap_: **remap** + +
+ ### **Trend** (19) * _Average Directional Movement Index_: **adx** @@ -961,7 +970,7 @@ import vectorbt as vbt df = pd.DataFrame().ta.ticker("AAPL") # requires 'yfinance' installed -# Create the "Golden Cross" +# Create the "Golden Cross" df["GC"] = df.ta.sma(50, append=True) > df.ta.sma(200, append=True) # Create boolean Signals(TS_Entries, TS_Exits) for vectorbt @@ -1005,7 +1014,7 @@ result = ta.cagr(df.close) # **Stochastic Samples**   _BETA_ Pandas TA can utilize the [stochastic](https://github.com/crflynn/stochastic) package (```pip install stochastic```) to Generate Sample Processes. For arguments and features, see ```help(ta.sample)``` -In short, when you create a Stochastic Sample, +In short, when you create a Stochastic Sample, ```python # Returns a Sample Realization Object @@ -1091,11 +1100,10 @@ help(ta.sample) # **Support** Feeling generous, like the package or want to see it become more a mature package? -* Donations help cover data and API costs so platform indicataors (like [TradingView](https://github.com/tradingview/)) are accurate. +* Donations help cover data and API costs so platform indicators (like [TradingView](https://github.com/tradingview/)) are accurate. * I appreciate **ALL** of those that have bought me Coffee/Beer/Wine et al. I greatly appreciate it! 😎
### Consider [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/twopirllc) - diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index 2de5989..c5279c5 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -2,6 +2,7 @@ name = "pandas_ta" """ .. moduleauthor:: Kevin Johnson """ + # Dictionaries and version from pandas_ta.maps import EXCHANGE_TZ, RATE, Category, Imports, version from pandas_ta.utils import * @@ -28,4 +29,4 @@ from pandas_ta.custom import create_dir, import_dir # Empty DataFrame Alias. Example: # >> ta.df.ta.ticker("spy") -df = DataFrame() \ No newline at end of file +df = DataFrame() diff --git a/pandas_ta/core.py b/pandas_ta/core.py index cfc7d55..693eaf6 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -12,9 +12,8 @@ from pandas.core.base import PandasObject from pandas.errors import PerformanceWarning from pandas import DataFrame, Series - from pandas_ta import * -from pandas_ta.utils import * +# from pandas_ta.utils import * # Base Class for extending a Pandas DataFrame @@ -1428,6 +1427,22 @@ class AnalysisIndicators(BasePandasObject): result = zscore(close=close, length=length, std=std, offset=offset, **kwargs) return self._post_process(result, **kwargs) + # Transform + def cube(self, cubing_exponent=None, signal_offset=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = cube(close=close, cubing_exponent=cubing_exponent, signal_offset=signal_offset, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + + def ifisher(self, amplifying_factor=None, signal_offset=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = ifisher(close=close, amplifying_factor=amplifying_factor, signal_offset=signal_offset, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + + def remap(self, from_min=None, from_max=None, to_min=None, to_max=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = remap(close=close, from_min=from_min, from_max=from_max, to_min=to_min, to_max=to_max, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + # Trend def adx(self, length=None, lensig=None, mamode=None, scalar=None, drift=None, offset=None, **kwargs): high = self._get_column(kwargs.pop("high", "high")) diff --git a/pandas_ta/transform/__init__.py b/pandas_ta/transform/__init__.py new file mode 100644 index 0000000..5e197a5 --- /dev/null +++ b/pandas_ta/transform/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from .cube import cube +from .ifisher import ifisher +from .remap import remap diff --git a/pandas_ta/transform/cube.py b/pandas_ta/transform/cube.py new file mode 100644 index 0000000..301f04f --- /dev/null +++ b/pandas_ta/transform/cube.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from pandas import DataFrame, Series +from pandas_ta.utils import get_offset, verify_series + + +def cube(close: Series, cubing_exponent: float = None, signal_offset: int = None, offset: int = None, **kwargs) -> DataFrame: + """ + Indicator: Cube Transform + + John Ehlers describes this indicator to be useful in compressing signals near zero for a normalized oscillator + like the Inverse Fisher Transform. In conjunction to that, values close to -1 and 1 are nearly unchanged, + whereas the ones near zero are reduced regarding their amplitude. + From the input data the effects of spectral dilation should have been removed (i.e. roofing filter). + + Sources: + Book: Cycle Analytics for Traders, 2014, written by John Ehlers, page 200 + Implemented by rengel8 for Pandas TA based on code of Markus K. (cryptocoinserver) + + Args: + close (pd.Series): Series of 'close's + cubing_exponent (float): Use this exponent 'wisely' to increase the impact of the soft limiter. Default: 3 + signal_offset (int): Offset the signal line. Default: -1 + offset (int): How many periods to offset the result. Default: 0 + + Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + + Returns: + pd.DataFrame: New feature generated. + """ + + # Validate arguments + close = verify_series(close) + cubing_exponent = float(cubing_exponent) if cubing_exponent and cubing_exponent >= 3.0 else 3.0 + signal_offset = int(signal_offset) if signal_offset and signal_offset > 0 else 1 + offset = get_offset(offset) + + # Calculate Result + result = close ** cubing_exponent + + cube_transform = Series(result, index=close.index) + cube_transform_signal = Series(result, index=close.index) + + # Offset + if offset != 0: + cube_transform = cube_transform.shift(offset) + cube_transform_signal = cube_transform_signal.shift(offset) + if signal_offset != 0: + cube_transform_signal = cube_transform_signal.shift(signal_offset) + + # Handle fills + if "fillna" in kwargs: + cube_transform.fillna(kwargs["fillna"], inplace=True) + cube_transform_signal.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + cube_transform.fillna(method=kwargs["fill_method"], inplace=True) + cube_transform_signal.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + cube_transform.name = f"CUBE" + cube_transform_signal.name = f"CUBE_SIGNAL" + cube_transform.category = cube_transform_signal.category = "transform" + + # Prepare DataFrame to return + data = {cube_transform.name: cube_transform, cube_transform_signal.name: cube_transform_signal} + df = DataFrame(data) + df.name = f"CUBE_TRANSFORM" + df.category = cube_transform.category + + return df diff --git a/pandas_ta/transform/ifisher.py b/pandas_ta/transform/ifisher.py new file mode 100644 index 0000000..f9211a0 --- /dev/null +++ b/pandas_ta/transform/ifisher.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from numpy import exp as npExp +from pandas import DataFrame, Series +from pandas_ta.utils import get_offset, verify_series + + +def ifisher(close: Series, amplifying_factor: float = None, signal_offset: int = None, offset: int = None, + **kwargs) -> DataFrame: + """ + Indicator: Inverse Fisher Transform + + John Ehlers describes this indicator as a tool to change the "Probability Distribution Function (PDF)" for + the results of known oscillator-indicators (time series) to receive clearer signals. + Its input needs to be normalized into the range from -1 to 1. Input data in the range of -0.5 to 0.5 + would not have a significant impact. Ehlers note's as an important fact that larger values will be transformed + or compressed stronger to the underlying unity of -1 to 1. + + Preparation Examples (or use 'remap'-indicator for this preparation): + (RSI - 50) * 0.1 RSI [0 to 100] -> -5 to 5 + (RSI - 50) * 0.02 RSI [0 to 100] -> -1 to 1, use amplifying_factor of 5 to match input of example above + + Sources: + https://www.mesasoftware.com/papers/TheInverseFisherTransform.pdf, + Book: Cycle Analytics for Traders, 2014, written by John Ehlers, page 198 + Implemented by rengel8 for Pandas TA based on code of Markus K. (cryptocoinserver) + + Args: + close (pd.Series): Series of 'close's + amplifying_factor (float): Use this factor to increase the impact of the soft limiter. Default: 1 + signal_offset (int): Offset the signal line. Default: -1 + offset (int): How many periods to offset the result. Default: 0 + + Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + + Returns: + pd.DataFrame: New feature generated. + """ + + # Validate arguments + close = verify_series(close) + amplifying_factor = float(amplifying_factor) if amplifying_factor and amplifying_factor != 0 else 1.0 + signal_offset = int(signal_offset) if signal_offset and signal_offset > 0 else 1 + offset = get_offset(offset) + + # Calculate Result + series = close.to_numpy() + result = (npExp(amplifying_factor * series) - 1) / (npExp(amplifying_factor * series) + 1) + + # Series + inv_fisher = Series(result, index=close.index) + inv_fisher_signal = Series(result, index=close.index) + + # Offset + if offset != 0: + inv_fisher = inv_fisher.shift(offset) + inv_fisher_signal = inv_fisher_signal.shift(offset) + if signal_offset != 0: + inv_fisher_signal = inv_fisher_signal.shift(signal_offset) # !!!! + + # Handle fills + if "fillna" in kwargs: + inv_fisher.fillna(kwargs["fillna"], inplace=True) + inv_fisher_signal.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + inv_fisher.fillna(method=kwargs["fill_method"], inplace=True) + inv_fisher_signal.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + inv_fisher.name = f"INV_FISHER" + inv_fisher_signal.name = f"INV_FISHER_SIGNAL" + inv_fisher.category = inv_fisher_signal.category = "transform" + + # Prepare DataFrame to return + data = {inv_fisher.name: inv_fisher, inv_fisher_signal.name: inv_fisher_signal} + df = DataFrame(data) + df.name = f"INVERSE_FISHER_TRANSFORM" + df.category = inv_fisher.category + + return df diff --git a/pandas_ta/transform/remap.py b/pandas_ta/transform/remap.py new file mode 100644 index 0000000..7f4b900 --- /dev/null +++ b/pandas_ta/transform/remap.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from pandas import Series +from pandas_ta.utils import get_offset, verify_series + + +def remap(close: Series, from_min: float = None, from_max: float = None, to_min: float = None, to_max: float = None, + offset: int = None, **kwargs) -> Series: + """ + Indicator: ReMap (REMAP) + + Basically a static normalizer, which maps the input min and max to a given output range. Many range bound + oscillators move between 0 and 100, but there are also other variants. Refer to the example below or add more the + list. + + Examples: + RSI -> IFISHER from_min=0, from_max=100, to_min=-1, to_max=1.0 + + Sources: + rengel8 for Pandas TA + + Args: + close (pd.Series): Series of 'close's + from_min (float): Input minimum. Default: 0 + from_max (float): Input maximum. Default: 100 + to_min (float): Output minimum. Default: 0 + to_max (float): Output maximum. Default: 100 + offset (int): How many periods to offset the result. Default: 0 + + Kwargs: + fillna (value, optional): pd.DataFrame.fillna(value) + fill_method (value, optional): Type of fill method + + Returns: + pd.Series: New feature generated. + """ + + # Validate arguments + close = verify_series(close) + from_min = float(from_min) if from_min and from_min != 0.0 else 0.0 + from_max = float(from_max) if from_max and from_max != 0.0 else 100.0 + to_min = float(to_min) if to_min and to_min != 0.0 else -1.0 + to_max = float(to_max) if to_max and to_max != 0.0 else 1.0 + offset = get_offset(offset) + + # Calculate Result + result = ((close - from_min) / (from_max - from_min)) * (to_max - to_min) + to_min + + # get Series + result = Series(result, index=close.index) + + # Offset + if offset != 0: + result = result.shift(offset) + + # Handle fills + if "fillna" in kwargs: + result.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + result.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + result.name = f"REMAP_{from_min}_{from_max}_{to_min}_{to_max}" + result.category = "transform" + + return result