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
[](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