diff --git a/.gitignore b/.gitignore index 21e8f1f..475e306 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,7 @@ pandas_ta/_wrapper.py AlphaVantageAPI/ data/datas.csv +data/f500.csv data/GLD_D_tv.csv data/SPY_5min.csv data/SPY_1min.csv diff --git a/Makefile b/Makefile index bfd9a8e..659bbe4 100644 --- a/Makefile +++ b/Makefile @@ -16,16 +16,16 @@ init: pip install -r requirements.txt test_ext: - python -m unittest -v tests/test_ext_indicator_*.py + python -m unittest -v -f tests/test_ext_indicator_*.py test_metrics: - python -m unittest -v tests/test_utils_metrics.py + python -m unittest -v -f tests/test_utils_metrics.py test_strats: - python -m unittest -v tests/test_strategy.py + python -m unittest -v -f tests/test_strategy.py test_ta: - python -m unittest -v tests/test_indicator_*.py + python -m unittest -v -f tests/test_indicator_*.py test_utils: - python -m unittest -v tests/test_utils.py \ No newline at end of file + python -m unittest -v -f tests/test_utils.py \ No newline at end of file diff --git a/README.md b/README.md index 405e68c..16a6051 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,12 @@ _Pandas Technical Analysis_ (**Pandas TA**) is an easy to use library that lever * [Utility](#utility-5) * [Volatility](#volatility-13) * [Volume](#volume-13) +* [Performance Metrics](#performance-metrics) * [Changes](#changes) - * [Recent](#recent) - * [Breaking](#breaking) - * [New](#new) - * [Updated](#updated) + * [General](#general) + * [Breaking Indicators](#breaking-indicators) + * [New Indicators](#new-indicators) + * [Updated Indicators](#updated-indicators) @@ -56,9 +57,11 @@ _Pandas Technical Analysis_ (**Pandas TA**) is an easy to use library that lever * Has 120+ indicators and utility functions. * Indicators are tightly correlated with the de facto [TA Lib](https://mrjbq7.github.io/ta-lib/) if they share common indicators. -* Have the need for speed? By using the _strategy_ method, you get **multiprocessing** for free! +* Have the need for speed? By using the DataFrame _strategy_ method, you get **multiprocessing** for free! * Easily add _prefixes_ or _suffixes_ or both to columns names. Useful for Custom Chained Strategies. * Example Jupyter Notebooks under the [examples](https://github.com/twopirllc/pandas-ta/tree/master/examples) directory, including how to create Custom Strategies using the new [__Strategy__ Class](https://github.com/twopirllc/pandas-ta/tree/master/examples/PandaTA_Strategy_Examples.ipynb) +* **NEW** Performance Metrics +
@@ -160,7 +163,7 @@ Thanks for trying **Pandas TA**! _Thank you for your contributions!_ -[alexonab](https://github.com/alexonab) | [allahyarzadeh](https://github.com/allahyarzadeh) | [codesutras](https://github.com/codesutras) | [daikts](https://github.com/daikts) | [DrPaprikaa](https://github.com/DrPaprikaa) | [FGU1](https://github.com/FGU1) | [lluissalord](https://github.com/lluissalord) | [maxdignan](https://github.com/maxdignan) | [NkosenhleDuma](https://github.com/NkosenhleDuma) | [pbrumblay](https://github.com/pbrumblay) | [rluong003](https://github.com/rluong003) | [SoftDevDanial](https://github.com/SoftDevDanial) | [tg12](https://github.com/tg12) | [YuvalWein](https://github.com/YuvalWein) +[alexonab](https://github.com/alexonab) | [allahyarzadeh](https://github.com/allahyarzadeh) | [codesutras](https://github.com/codesutras) | [daikts](https://github.com/daikts) | [DrPaprikaa](https://github.com/DrPaprikaa) | [FGU1](https://github.com/FGU1) | [lluissalord](https://github.com/lluissalord) | [maxdignan](https://github.com/maxdignan) | [NkosenhleDuma](https://github.com/NkosenhleDuma) | [pbrumblay](https://github.com/pbrumblay) | [RajeshDhalange](https://github.com/RajeshDhalange) | [rluong003](https://github.com/rluong003) | [SoftDevDanial](https://github.com/SoftDevDanial) | [tg12](https://github.com/tg12) | [YuvalWein](https://github.com/YuvalWein)
@@ -174,21 +177,21 @@ _Conventional_ You explicitly define the input columns and take care of the output. * ```sma10 = ta.sma(df["Close"], length=10)``` - * Returns a series with name: ```SMA_10``` + * Returns a Series with name: ```SMA_10``` * ```donchiandf = ta.donchian(df["HIGH"], df["low"], lower_length=10, upper_length=15)``` * Returns a DataFrame named ```DC_10_15``` and column names: ```DCL_10_15, DCM_10_15, DCU_10_15``` * ```ema10_ohlc4 = ta.ema(ta.ohlc4(df["Open"], df["High"], df["Low"], df["Close"]), length=10)``` * Conventional Chaining is possible but more explicit. - * Since it returns a series named ```EMA_10```. If needed, you may need to uniquely name it. + * Since it returns a Series named ```EMA_10```. If needed, you may need to uniquely name it. _Pandas TA DataFrame Extension_ ==================== Calling ```df.ta``` will automatically lowercase _OHLCVA_ to _ohlcva_: _open, high, low, close, volume_, _adj_close_. By default, ```df.ta``` will use the _ohlcva_ for the indicator arguments removing the need to specify input columns directly. * ```sma10 = df.ta.sma(length=10)``` - * Returns a series with name: ```SMA_10``` + * Returns a Series with name: ```SMA_10``` * ```ema10_ohlc4 = df.ta.ema(close=df.ta.ohlc4(), length=10, suffix="OHLC4")``` - * Returns a series with name: ```EMA_10_OHLC4``` + * Returns a Series with name: ```EMA_10_OHLC4``` * Chaining Indicators _require_ specifying the input like: ```close=df.ta.ohlc4()```. * ```donchiandf = df.ta.donchian(lower_length=10, upper_length=15)``` * Returns a DataFrame named ```DC_10_15``` and column names: ```DCL_10_15, DCM_10_15, DCU_10_15``` @@ -356,18 +359,18 @@ df.ta.strategy(NonMPStrategy) ## **adjusted** ```python -# Set ta to default to an adjusted column, 'adj_close', overriding default 'close' +# Set ta to default to an adjusted column, 'adj_close', overriding default 'close'. df.ta.adjusted = "adj_close" df.ta.sma(length=10, append=True) -# To reset back to 'close', set adjusted back to None +# To reset back to 'close', set adjusted back to None. df.ta.adjusted = None ``` ## **categories** ```python -# List of Pandas TA categories +# List of Pandas TA categories. df.ta.categories ``` @@ -375,7 +378,7 @@ df.ta.categories ```python # Set the number of cores to use for strategy multiprocessing -# Defaults to the number of cpus you have +# Defaults to the number of cpus you have. df.ta.cores = 4 # Returns the number of cores you set or your default number of cpus. @@ -386,36 +389,31 @@ df.ta.cores ```python # The 'datetime_ordered' property returns True if the DataFrame -# index is of Pandas datetime64 and df.index[0] < df.index[-1] -# Otherwise it returns False +# index is of Pandas datetime64 and df.index[0] < df.index[-1]. +# Otherwise it returns False. df.ta.datetime_ordered ``` ## **reverse** ```python -# The 'datetime_ordered' property returns True if the DataFrame -# index is of Pandas datetime64 and df.index[0] < df.index[-1] -# Otherwise it returns False -df.ta.datetime_ordered - # The 'reverse' is a helper property that returns the DataFrame -# in reverse order +# in reverse order. df.ta.reverse ``` ## **prefix & suffix** ```python -# Applying a prefix to the name of an indicator +# Applying a prefix to the name of an indicator. prehl2 = df.ta.hl2(prefix="pre") print(prehl2.name) # "pre_HL2" -# Applying a suffix to the name of an indicator +# Applying a suffix to the name of an indicator. endhl2 = df.ta.hl2(suffix="post") print(endhl2.name) # "HL2_post" -# Applying a prefix and suffix to the name of an indicator +# Applying a prefix and suffix to the name of an indicator. bothhl2 = df.ta.hl2(prefix="pre", suffix="post") print(bothhl2.name) # "pre_HL2_post" ``` @@ -613,22 +611,45 @@ Use parameter: cumulative=**True** for cumulative results. |:--------:| | ![Example OBV](/images/SPY_OBV.png) | +

+ +# **Performance Metrics** +_Performance Metrics_ are a **new** addition to the package. These metrics return a _float_ and are _not_ part of the _DataFrame_ Extension. They are called conventionally. For Example: +```python +import pandas_ta as ta +result = ta.cagr(df.close) +``` + +### Available Metrics +* _Compounded Annual Growth Rate_: **cagr** +* _Calmar Ratio_: **calmar_ratio** +* _Downside Deviation_: **downside_deviation** +* _Jensen's Alpha_: **jensens_alpha** +* _Log Max Drawdown_: **log_max_drawdown** +* _Max Drawdown_: **max_drawdown** +* _Pure Profit Score_: **pure_profit_score** +* _Sharpe Ratio_: **sharpe_ratio** +* _Sortino Ratio_: **sortino_ratio** +* _Volatility_: **volatility** + +

# **Changes** -## **Recent** +## **General** * A __Strategy__ Class to help name and group your favorite indicators. * Some indicators have had their ```mamode``` _kwarg_ updated with more _moving average_ choices with the **Moving Average Utility** function ```ta.ma()```. For simplicity, all _choices_ are single source _moving averages_. This is primarily an internal utility used by indicators that have a ```mamode``` _kwarg_. This includes indicators: _accbands_, _amat_, _aobv_, _atr_, _bbands_, _bias_, _efi_, _hilo_, _kc_, _natr_, _qqe_, _rvi_, and _thermo_; the default ```mamode``` parameters have not changed. However, ```ta.ma()``` can be used by the user as well if needed. For more information: ```help(ta.ma)``` * **Moving Average Choices**: dema, ema, fwma, hma, linreg, midpoint, pwma, rma, sinwma, sma, swma, t3, tema, trima, vidya, wma, zlma. * An _experimental_ and independent __Watchlist__ Class located in the [Examples](https://github.com/twopirllc/pandas-ta/tree/master/examples/watchlist.py) Directory that can be used in conjunction with the new __Strategy__ Class. * _Linear Regression_ (**linear_regression**) is a new utility method for Simple Linear Regression using _Numpy_ or _Scikit Learn_'s implementation. +
-## **Breaking** +## **Breaking Indicators** * _Bollinger Bands_ (**bbands**): New column 'bandwidth' appended to the returning DataFrame. See: ```help(ta.bbands)``` -## **New** +## **New Indicators** * _Drawdown_ (**drawdown**) It is a peak-to-trough decline during a specific period for an investment, trading account, or fund. See: ```help(ta.drawdown)``` * _Gann High-Low Activator_ (**hilo**) The Gann High Low Activator Indicator was created by Robert Krausz in a 1998. See: ```help(ta.hilo)``` @@ -638,10 +659,13 @@ trading account, or fund. See: ```help(ta.drawdown)``` * _TTM Trend_ (**ttm_trend**). A trend indicator inspired from John Carter's book "Mastering the Trade" issue of Stocks & Commodities Magazine. It is a moving average based trend indicator consisting of two different simple moving averages. See: ```help(ta.ttm_trend)``` * _Variable Index Dynamic Average_ (**vidya**) A popular Dynamic Moving Average created by Tushar Chande. See: ```help(ta.vidya)``` -## **Updated** +## **Updated Indicators** * _Average True Range_ (**atr**): The default ```mamode``` is now "**RMA**" and with the same ```mamode``` options as TradingView. See ```help(ta.atr)```. +* _Decreasing_ (**decreasing**): New argument ```strict``` checks if the series is continuously decreasing over period ```length```. Default: ```False```. See ```help(ta.decreasing)```. +* _Increasing_ (**increasing**): New argument ```strict``` checks if the series is continuously increasing over period ```length```. Default: ```False```. See ```help(ta.increasing)```. * _Trend Return_ (**trend_return**): Returns a DataFrame now instead of Series with pertinenet trade info for a _trend_. An example can be found in the [AI Example Notebook](https://github.com/twopirllc/pandas-ta/tree/master/examples/AIExample.ipynb). The notebook is still a work in progress and open to colloboration. +
# **Sources** * [Original TA-LIB](http://ta-lib.org/) diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 35fc3a6..6534c9c 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -185,17 +185,17 @@ class AnalysisIndicators(BasePandasObject): >>> ichimoku, span = ta.ichimoku(df["High"], df["Low"], df["Close"]) Args: - kind (str, optional): Default: None. Kind is the 'name' of the indicator. + kind (str, optional): Default: None. Kind is the 'name' of the indicator. It converts kind to lowercase before calling. - timed (bool, optional): Default: False. Curious about the execution + timed (bool, optional): Default: False. Curious about the execution speed? kwargs: Extension specific modifiers. - append (bool, optional): Default: False. When True, it appends the + append (bool, optional): Default: False. When True, it appends the resultant column(s) to the DataFrame. Returns: - Most Indicators will return a Pandas Series. Others like MACD, BBANDS, - KC, et al will return a Pandas DataFrame. Ichimoku on the other hand + Most Indicators will return a Pandas Series. Others like MACD, BBANDS, + KC, et al will return a Pandas DataFrame. Ichimoku on the other hand will return two DataFrames, the Ichimoku DataFrame for the known period and a Span DataFrame for the future of the Span values. @@ -1239,9 +1239,9 @@ class AnalysisIndicators(BasePandasObject): result = decay(close=close, length=length, mode=mode, offset=offset, **kwargs) return self._post_process(result, **kwargs) - def decreasing(self, length=None, asint=True, offset=None, **kwargs): + def decreasing(self, length=None, strict=None, asint=None, offset=None, **kwargs): close = self._get_column(kwargs.pop("close", "close")) - result = decreasing(close=close, length=length, asint=asint, offset=offset, **kwargs) + result = decreasing(close=close, length=length, strict=strict, asint=asint, offset=offset, **kwargs) return self._post_process(result, **kwargs) def dpo(self, length=None, centered=True, offset=None, **kwargs): @@ -1249,9 +1249,9 @@ class AnalysisIndicators(BasePandasObject): result = dpo(close=close, length=length, centered=centered, offset=offset, **kwargs) return self._post_process(result, **kwargs) - def increasing(self, length=None, asint=True, offset=None, **kwargs): + def increasing(self, length=None, strict=None, asint=None, offset=None, **kwargs): close = self._get_column(kwargs.pop("close", "close")) - result = increasing(close=close, length=length, asint=asint, offset=offset, **kwargs) + result = increasing(close=close, length=length, strict=strict, asint=asint, offset=offset, **kwargs) return self._post_process(result, **kwargs) def long_run(self, fast=None, slow=None, length=None, offset=None, **kwargs): diff --git a/pandas_ta/momentum/ppo.py b/pandas_ta/momentum/ppo.py index a3bcbf0..829a62a 100644 --- a/pandas_ta/momentum/ppo.py +++ b/pandas_ta/momentum/ppo.py @@ -14,7 +14,6 @@ def ppo(close, fast=None, slow=None, signal=None, scalar=None, offset=None, **kw scalar = float(scalar) if scalar else 100 if slow < fast: fast, slow = slow, fast - min_periods = int(kwargs["min_periods"]) if "min_periods" in kwargs and kwargs["min_periods"] is not None else fast offset = get_offset(offset) # Calculate Result diff --git a/pandas_ta/overlap/fwma.py b/pandas_ta/overlap/fwma.py index 8e90fb1..8859217 100644 --- a/pandas_ta/overlap/fwma.py +++ b/pandas_ta/overlap/fwma.py @@ -7,7 +7,6 @@ def fwma(close, length=None, asc=None, offset=None, **kwargs): # Validate Arguments close = verify_series(close) length = int(length) if length and length > 0 else 10 - min_periods = int(kwargs["min_periods"]) if "min_periods" in kwargs and kwargs["min_periods"] is not None else length asc = asc if asc else True offset = get_offset(offset) diff --git a/pandas_ta/overlap/linreg.py b/pandas_ta/overlap/linreg.py index 54f53e6..f77e55d 100644 --- a/pandas_ta/overlap/linreg.py +++ b/pandas_ta/overlap/linreg.py @@ -8,7 +8,6 @@ def linreg(close, length=None, offset=None, **kwargs): # Validate arguments close = verify_series(close) length = int(length) if length and length > 0 else 14 - min_periods = int(kwargs["min_periods"]) if "min_periods" in kwargs and kwargs["min_periods"] is not None else length offset = get_offset(offset) angle = kwargs.pop("angle", False) intercept = kwargs.pop("intercept", False) diff --git a/pandas_ta/statistics/entropy.py b/pandas_ta/statistics/entropy.py index 9fb5996..c3e05ef 100644 --- a/pandas_ta/statistics/entropy.py +++ b/pandas_ta/statistics/entropy.py @@ -45,9 +45,9 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 10 - base (float): Logarithmic Base. Default: 2 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 10 + base (float): Logarithmic Base. Default: 2 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/kurtosis.py b/pandas_ta/statistics/kurtosis.py index e2ed56c..8b477f2 100644 --- a/pandas_ta/statistics/kurtosis.py +++ b/pandas_ta/statistics/kurtosis.py @@ -36,8 +36,8 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 30 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/mad.py b/pandas_ta/statistics/mad.py index 3c871d8..8d813af 100644 --- a/pandas_ta/statistics/mad.py +++ b/pandas_ta/statistics/mad.py @@ -41,8 +41,8 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 30 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/median.py b/pandas_ta/statistics/median.py index b58ba0d..023dbcb 100644 --- a/pandas_ta/statistics/median.py +++ b/pandas_ta/statistics/median.py @@ -39,8 +39,8 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 30 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/quantile.py b/pandas_ta/statistics/quantile.py index 6c6e6d9..543c972 100644 --- a/pandas_ta/statistics/quantile.py +++ b/pandas_ta/statistics/quantile.py @@ -37,9 +37,9 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 - q (float): The quantile. Default: 0.5 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 30 + q (float): The quantile. Default: 0.5 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/skew.py b/pandas_ta/statistics/skew.py index 0920b16..560bbd4 100644 --- a/pandas_ta/statistics/skew.py +++ b/pandas_ta/statistics/skew.py @@ -42,8 +42,8 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 30 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/stdev.py b/pandas_ta/statistics/stdev.py index 825f3e5..cc50662 100644 --- a/pandas_ta/statistics/stdev.py +++ b/pandas_ta/statistics/stdev.py @@ -39,11 +39,11 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 + length (int): It's period. Default: 30 ddof (int): Delta Degrees of Freedom. The divisor used in calculations is N - ddof, where N represents the number of elements. Default: 1 - offset (int): How many periods to offset the result. Default: 0 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/variance.py b/pandas_ta/statistics/variance.py index 31c4105..b61afb8 100644 --- a/pandas_ta/statistics/variance.py +++ b/pandas_ta/statistics/variance.py @@ -38,11 +38,11 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 + length (int): It's period. Default: 30 ddof (int): Delta Degrees of Freedom. The divisor used in calculations is N - ddof, where N represents the number of elements. Default: 1 - offset (int): How many periods to offset the result. Default: 0 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/statistics/zscore.py b/pandas_ta/statistics/zscore.py index 82c494a..9ff2897 100644 --- a/pandas_ta/statistics/zscore.py +++ b/pandas_ta/statistics/zscore.py @@ -44,9 +44,9 @@ Calculation: Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 30 - std (float): It's period. Default: 1 - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 30 + std (float): It's period. Default: 1 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/trend/decreasing.py b/pandas_ta/trend/decreasing.py index 159cdc0..695ac2f 100644 --- a/pandas_ta/trend/decreasing.py +++ b/pandas_ta/trend/decreasing.py @@ -2,18 +2,27 @@ from pandas_ta.utils import get_offset, verify_series -def decreasing(close, length=None, asint=True, strictly=False, offset=None, **kwargs): +def decreasing(close, length=None, strict=None, asint=None, offset=None, **kwargs): """Indicator: Decreasing""" # Validate Arguments close = verify_series(close) length = int(length) if length and length > 0 else 1 + strict = strict if isinstance(strict, bool) else False + asint = asint if isinstance(asint, bool) else True offset = get_offset(offset) + def stricly_decreasing(series, n): + return all([i > j for i,j in zip(series[-n:], series[1:])]) + # Calculate Result - if strictly: - decreasing = all(i > j for i, j in zip(close[-length:], close[1:])) + if strict: + # Returns value as float64? Have to cast to bool + decreasing = close.rolling(length, min_periods=length).apply(stricly_decreasing, args=(length,), raw=False) + decreasing.fillna(0, inplace=True) + decreasing = decreasing.astype(bool) else: decreasing = close.diff(length) < 0 + if asint: decreasing = decreasing.astype(int) @@ -28,7 +37,7 @@ def decreasing(close, length=None, asint=True, strictly=False, offset=None, **kw decreasing.fillna(method=kwargs["fill_method"], inplace=True) # Name and Categorize it - decreasing.name = f"DEC_{length}" + decreasing.name = f"{'S' if strict else ''}DEC_{length}" decreasing.category = "trend" return decreasing @@ -37,22 +46,23 @@ def decreasing(close, length=None, asint=True, strictly=False, offset=None, **kw decreasing.__doc__ = \ """Decreasing -Returns True or False if the series is decreasing over a periods. By default, -it returns True and False as 1 and 0 respectively with kwarg 'asint'. - -Sources: +Returns True if the series is decreasing over a period, False otherwise. If the kwarg 'strict' is True, it returns True if it is continuously decreasing over the period. When using the kwarg 'asint', then it returns 1 for True or 0 for False. Calculation: - decreasing = close.diff(length) < 0 + if strict: + decreasing = all(i > j for i, j in zip(close[-length:], close[1:])) + else: + decreasing = close.diff(length) < 0 + if asint: decreasing = decreasing.astype(int) Args: close (pd.Series): Series of 'close's - length (int): It's period. Default: 1 - asint (bool): Returns as binary. Default: True - strictly (bool): If True check for strictly continuous decreasing Default: False - offset (int): How many periods to offset the result. Default: 0 + length (int): It's period. Default: 1 + asint (bool): Returns as binary. Default: True + strict (bool): If True, checks if the series is continuously decreasing over the period. Default: False + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/trend/increasing.py b/pandas_ta/trend/increasing.py index 57e9300..83b0373 100644 --- a/pandas_ta/trend/increasing.py +++ b/pandas_ta/trend/increasing.py @@ -2,22 +2,30 @@ from pandas_ta.utils import get_offset, verify_series -def increasing(close, length=None, asint=True, strictly=False, offset=None, **kwargs): +def increasing(close, length=None, strict=None, asint=None, offset=None, **kwargs): """Indicator: Increasing""" # Validate Arguments close = verify_series(close) length = int(length) if length and length > 0 else 1 + strict = strict if isinstance(strict, bool) else False + asint = asint if isinstance(asint, bool) else True offset = get_offset(offset) - + + def stricly_increasing(series, n): + return all([i < j for i,j in zip(series[-n:], series[1:])]) + # Calculate Result - if strictly: - increasing = all(i < j for i, j in zip(close[-length:], close[1:])) + if strict: + # Returns value as float64? Have to cast to bool + increasing = close.rolling(length, min_periods=length).apply(stricly_increasing, args=(length,), raw=False) + increasing.fillna(0, inplace=True) + increasing = increasing.astype(bool) else: increasing = close.diff(length) > 0 - + if asint: increasing = increasing.astype(int) - + # Offset if offset != 0: increasing = increasing.shift(offset) @@ -29,35 +37,37 @@ def increasing(close, length=None, asint=True, strictly=False, offset=None, **kw increasing.fillna(method=kwargs["fill_method"], inplace=True) # Name and Categorize it - increasing.name = f"INC_{length}" + increasing.name = f"{'S' if strict else ''}INC_{length}" increasing.category = "trend" return increasing -increasing.__doc__ = """Increasing +increasing.__doc__ = \ +"""Increasing - Returns True or False if the series is increasing over a periods. By default, - it returns True and False as 1 and 0 respectively with kwarg 'asint'. - - Sources: - - Calculation: +Returns True if the series is increasing over a period, False otherwise. If the kwarg 'strict' is True, it returns True if it is continuously increasing over the period. When using the kwarg 'asint', then it returns 1 for True or 0 for False. + +Calculation: + if strict: + increasing = all(i < j for i, j in zip(close[-length:], close[1:])) + else: increasing = close.diff(length) > 0 - if asint: - increasing = increasing.astype(int) - - Args: - close (pd.Series): Series of 'close's - length (int): It's period. Default: 1 - asint (bool): Returns as binary. Default: True - strictly (bool): If True check for strictly continuous increasing Default: False - 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. - """ + + if asint: + increasing = increasing.astype(int) + +Args: + close (pd.Series): Series of 'close's + length (int): It's period. Default: 1 + asint (bool): Returns as binary. Default: True + strict (bool): If True, checks if the series is continuously increasing over the period. Default: False + 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. +""" diff --git a/pandas_ta/utils/_metrics.py b/pandas_ta/utils/_metrics.py index a638f4e..947b648 100644 --- a/pandas_ta/utils/_metrics.py +++ b/pandas_ta/utils/_metrics.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from numpy import arange as npArange +# from numpy import arange as npArange from numpy import log as npLog from numpy import sqrt as npSqrt from pandas import DataFrame, Series, Timedelta @@ -11,15 +11,32 @@ from pandas_ta.performance import drawdown, log_return, percent_return def cagr(close: Series) -> float: - """Compounded Annual Growth Rate""" + """Compounded Annual Growth Rate + + Args: + close (pd.Series): Series of 'close's + + >>> result = ta.cagr(df.close) + """ close = verify_series(close) start, end = close.iloc[0], close.iloc[-1] return ((end / start) ** (1 / total_time(close))) - 1 -def calmar_ratio(close: Series, method: str = "percent", years: int = 3, log: bool = False) -> float: +def calmar_ratio(close: Series, method: str = "percent", years: int = 3) -> float: """The Calmar Ratio is the percent Max Drawdown Ratio 'typically' over - the past three years.""" + the past three years. + + Args: + close (pd.Series): Series of 'close's + method (str): Max DD calculation options: 'dollar', 'percent', 'log'. Default: 'dollar' + years (int): The positive number of years to use. Default: 3 + + >>> result = ta.calmar_ratio(close, method="percent", years=3) + """ + if years <= 0: + print(f"[!] calmar_ratio 'years' argument must be greater than zero.") + return close = verify_series(close) n_years_ago = close.index[-1] - Timedelta(days=365.25 * years) @@ -28,10 +45,18 @@ def calmar_ratio(close: Series, method: str = "percent", years: int = 3, log: bo return cagr(close) / max_drawdown(close, method=method) -def downside_deviation(returns: Series, benchmark_rate: float = 0.0, log: bool = False, tf: str = "years") -> float: +def downside_deviation(returns: Series, benchmark_rate: float = 0.0, tf: str = "years") -> float: """Downside Deviation for the Sortino ratio. Benchmark rate is assumed to be annualized. Adjusted according for the - number of periods per year seen in the data.""" + number of periods per year seen in the data. + + Args: + close (pd.Series): Series of 'close's + benchmark_rate (float): Benchmark Rate to use. Default: 0.0 + tf (str): Time Frame options: 'days', 'weeks', 'months', and 'years'. Default: 'years' + + >>> result = ta.downside_deviation(returns, benchmark_rate=0.0, tf="years") + """ # For both de-annualizing the benchmark rate and annualizing result returns = verify_series(returns) days_per_year = returns.shape[0] / total_time(returns, tf) @@ -44,8 +69,15 @@ def downside_deviation(returns: Series, benchmark_rate: float = 0.0, log: bool = return downside_deviation * npSqrt(days_per_year) -def jensens_alpha(returns:Series, benchmark_returns:Series) -> float: - """Jensen's 'Alpha' of a series and a benchmark.""" +def jensens_alpha(returns: Series, benchmark_returns: Series) -> float: + """Jensen's 'Alpha' of a series and a benchmark. + + Args: + returns (pd.Series): Series of 'returns's + benchmark_returns (pd.Series): Series of 'benchmark_returns's + + >>> result = ta.jensens_alpha(returns, benchmark_returns) + """ returns = verify_series(returns) benchmark_returns = verify_series(benchmark_returns) @@ -53,15 +85,29 @@ def jensens_alpha(returns:Series, benchmark_returns:Series) -> float: return linear_regression(benchmark_returns, returns)["a"] -def log_max_drawdown(close:Series): - """Log Max Drawdown of a series.""" +def log_max_drawdown(close: Series) -> float: + """Log Max Drawdown of a series. + + Args: + close (pd.Series): Series of 'close's + + >>> result = ta.log_max_drawdown(close) + """ close = verify_series(close) log_return = npLog(close.iloc[-1]) - npLog(close.iloc[0]) return log_return - max_drawdown(close, method="log") def max_drawdown(close: Series, method:str = None, all:bool = False) -> float: - """Maximum Drawdown from close. Defaults to 'dollar'. """ + """Maximum Drawdown from close. Default: 'dollar'. + + Args: + close (pd.Series): Series of 'close's + method (str): Max DD calculation options: 'dollar', 'percent', 'log'. Default: 'dollar' + all (bool): If True, it returns all three methods as a dict. Default: False + + >>> result = ta.max_drawdown(close, method="dollar", all=False) + """ close = verify_series(close) max_dd = drawdown(close).max() @@ -77,9 +123,15 @@ def max_drawdown(close: Series, method:str = None, all:bool = False) -> float: return max_dd_["dollar"] -def pure_profit_score(close:Series) -> float: - """Pure Profit Score of a series.""" - from sklearn.linear_model import LinearRegression +def pure_profit_score(close: Series) -> float: + """Pure Profit Score of a series. + + Args: + close (pd.Series): Series of 'close's + + >>> result = ta.pure_profit_score(df.close) + """ + # from sklearn.linear_model import LinearRegression close = verify_series(close) close_index = Series(0, index=close.reset_index().index) @@ -87,8 +139,16 @@ def pure_profit_score(close:Series) -> float: return r * cagr(close) -def sharpe_ratio(close:Series, benchmark_rate:float = 0.0, log:bool = False) -> float: - """Sharpe Ratio of a series.""" +def sharpe_ratio(close: Series, benchmark_rate: float = 0.0, log: bool = False) -> float: + """Sharpe Ratio of a series. + + Args: + close (pd.Series): Series of 'close's + benchmark_rate (float): Benchmark Rate to use. Default: 0.0 + log (bool): If True, calculates log_return. Otherwise it returns percent_return. Default: False + + >>> result = ta.sharpe_ratio(close, benchmark_rate=0.0, log=False) + """ close = verify_series(close) returns = percent_return(close=close) if not log else log_return(close=close) @@ -97,8 +157,16 @@ def sharpe_ratio(close:Series, benchmark_rate:float = 0.0, log:bool = False) -> return result -def sortino_ratio(close:Series, benchmark_rate:float = 0.0, log:bool = False) -> float: - """Sortino Ratio of a series.""" +def sortino_ratio(close: Series, benchmark_rate: float = 0.0, log: bool = False) -> float: + """Sortino Ratio of a series. + + Args: + close (pd.Series): Series of 'close's + benchmark_rate (float): Benchmark Rate to use. Default: 0.0 + log (bool): If True, calculates log_return. Otherwise it returns percent_return. Default: False + + >>> result = ta.sortino_ratio(close, benchmark_rate=0.0, log=False) + """ close = verify_series(close) returns = percent_return(close=close) if not log else log_return(close=close) @@ -107,12 +175,23 @@ def sortino_ratio(close:Series, benchmark_rate:float = 0.0, log:bool = False) -> return result -def volatility(close: Series, tf:str = "years", returns:bool = False, log: bool = False, **kwargs) -> float: - """Volatility of a series. Default: 'years'""" +def volatility(close: Series, tf: str = "years", returns: bool = False, log: bool = False, **kwargs) -> float: + """Volatility of a series. Default: 'years' + + Args: + close (pd.Series): Series of 'close's + tf (str): Time Frame options: 'days', 'weeks', 'months', and 'years'. Default: 'years' + returns (bool): If True, then it replace the close Series with the user defined Series; typically user generated returns or percent returns or log returns. Default: False + log (bool): If True, calculates log_return. Otherwise it calculates percent_return. Default: False + + >>> result = ta.volatility(close, tf="years", returns=False, log=False, **kwargs) + """ close = verify_series(close) if not returns: returns = percent_return(close=close) if not log else log_return(close=close) + else: + returns = close factor = returns.shape[0] / total_time(returns, tf) if kwargs.pop("nearest_day", False) and tf.lower() == "years": diff --git a/setup.py b/setup.py index 206db2b..867c740 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( "pandas_ta.volatility", "pandas_ta.volume" ], - version=".".join(("0", "2", "29b")), + version=".".join(("0", "2", "30b")), description=long_description, long_description=long_description, author="Kevin Johnson", diff --git a/tests/test_ext_indicator_trend.py b/tests/test_ext_indicator_trend.py index 60a1c2e..6e73504 100644 --- a/tests/test_ext_indicator_trend.py +++ b/tests/test_ext_indicator_trend.py @@ -57,6 +57,10 @@ class TestTrendExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], "DEC_1") + self.data.ta.decreasing(length=3, strict=True, append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], "SDEC_3") + def test_dpo_ext(self): self.data.ta.dpo(append=True) self.assertIsInstance(self.data, DataFrame) @@ -67,6 +71,10 @@ class TestTrendExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], "INC_1") + self.data.ta.increasing(length=3, strict=True, append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], "SINC_3") + def test_long_run_ext(self): # Nothing passed, return self self.assertEqual(self.data.ta.long_run(append=True).shape, self.data.shape) diff --git a/tests/test_indicator_trend.py b/tests/test_indicator_trend.py index 0b2017c..c644b1d 100644 --- a/tests/test_indicator_trend.py +++ b/tests/test_indicator_trend.py @@ -113,6 +113,10 @@ class TestTrend(TestCase): self.assertIsInstance(result, Series) self.assertEqual(result.name, "DEC_1") + result = pandas_ta.decreasing(self.close, length=3, strict=True) + self.assertIsInstance(result, Series) + self.assertEqual(result.name, "SDEC_3") + def test_dpo(self): result = pandas_ta.dpo(self.close) self.assertIsInstance(result, Series) @@ -123,6 +127,10 @@ class TestTrend(TestCase): self.assertIsInstance(result, Series) self.assertEqual(result.name, "INC_1") + result = pandas_ta.increasing(self.close, length=3, strict=True) + self.assertIsInstance(result, Series) + self.assertEqual(result.name, "SINC_3") + def test_long_run(self): result = pandas_ta.long_run(self.close, self.open) self.assertIsInstance(result, Series) diff --git a/tests/test_utils_metrics.py b/tests/test_utils_metrics.py index db6233b..95d7d76 100644 --- a/tests/test_utils_metrics.py +++ b/tests/test_utils_metrics.py @@ -36,6 +36,12 @@ class TestUtilityMetrics(TestCase): self.assertIsInstance(result, float) self.assertGreaterEqual(result, 0) + result = pandas_ta.calmar_ratio(self.close, years=0) + self.assertIsNone(result) + + result = pandas_ta.calmar_ratio(self.close, years=-2) + self.assertIsNone(result) + def test_downside_deviation(self): result = pandas_ta.downside_deviation(self.pctret) self.assertIsInstance(result, float) @@ -101,6 +107,11 @@ class TestUtilityMetrics(TestCase): self.assertGreaterEqual(result, 0) def test_volatility(self): + returns_ = pandas_ta.percent_return(self.close) + result = pandas_ta.utils.volatility(returns_, returns=True) + self.assertIsInstance(result, float) + self.assertGreaterEqual(result, 0) + for tf in ["years", "months", "weeks", "days", "hours", "minutes", "seconds"]: result = pandas_ta.utils.volatility(self.close, tf) self.assertIsInstance(result, float)