From b5ab65cbeaa12677ebcd445291a4986ba26ccb0f Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Tue, 8 Sep 2020 11:34:29 -0700 Subject: [PATCH] ENH #117 #109 inside bar added MAINT utils refactoring --- README.md | 5 +- pandas_ta/__init__.py | 2 +- pandas_ta/candles/__init__.py | 3 +- pandas_ta/candles/cdl_inside.py | 74 +++++++++++++++++++ pandas_ta/core.py | 12 +++- pandas_ta/momentum/inbar.py | 61 ---------------- pandas_ta/overlap/dema.py | 9 ++- pandas_ta/utils.py | 112 +++++++++++++++-------------- tests/test_indicator_candle.py | 9 +++ tests/test_indicator_candle_ext.py | 17 +++-- 10 files changed, 174 insertions(+), 130 deletions(-) create mode 100644 pandas_ta/candles/cdl_inside.py delete mode 100644 pandas_ta/momentum/inbar.py diff --git a/README.md b/README.md index c298046..15469b8 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ and _Weighted Moving Average_. issue of Stocks & Commodities Magazine. It is a moving average based trend indicator consisting of two different simple moving averages. * _Stochastic RSI_ (**stochrsi**) "Stochastic RSI and Dynamic Momentum Index" was created by Tushar Chande and Stanley Kroll. In line with Trading View's calculation. See: ```help(ta.stochrsi)``` +* _Inside Bar_ (**cdl_inside**) An Inside Bar is a bar contained within it's previous bar's high and low See: ```help(ta.cdl_inside)``` ## __Updated Indicators__ * _Fisher Transform_ (**fisher**): Added Fisher's default **ema** signal line. To change the length of the signal line, use the argument: ```signal=5```. Default: 5 @@ -320,9 +321,10 @@ print(bothhl2.name) # "pre_HL2_post" # __Technical Analysis Indicators__ (_by Category_) -## _Candles_ (2) +## _Candles_ (3) * _Doji_: **cdl_doji** +* _Inside Bar_: **cdl_inside** * _Heikin-Ashi_: **ha** ## _Momentum_ (33) @@ -504,6 +506,7 @@ Use parameter: cumulative=**True** for cumulative results. # Contributors * [alexonab](https://github.com/alexonab) * [allahyarzadeh](https://github.com/allahyarzadeh) +* [DrPaprikaa](https://github.com/DrPaprikaa) * [FGU1](https://github.com/FGU1) * [lluissalord](https://github.com/lluissalord) * [SoftDevDanial](https://github.com/SoftDevDanial) diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index 071b785..e8db551 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -22,7 +22,7 @@ else: # Will find a dynamic solution later. Category = { # Candles - "candles": ["cdl_doji", "ha"], + "candles": ["cdl_doji", "cdl_inside", "ha"], # Momentum "momentum": ["ao", "apo", "bias", "bop", "brar", "cci", "cg", "cmo", "coppock", "er", "eri", "fisher", "inertia", "kdj", "kst", "macd", "mom", "pgo", "ppo", "psl", "pvo", "roc", "rsi", "rvgi", "slope", "smi", "squeeze", "stoch", "stochrsi", "trix", "tsi", "uo", "willr"], diff --git a/pandas_ta/candles/__init__.py b/pandas_ta/candles/__init__.py index d92eb5e..1657062 100644 --- a/pandas_ta/candles/__init__.py +++ b/pandas_ta/candles/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from .ha import ha -from .cdl_doji import cdl_doji \ No newline at end of file +from .cdl_doji import cdl_doji +from .cdl_inside import cdl_inside \ No newline at end of file diff --git a/pandas_ta/candles/cdl_inside.py b/pandas_ta/candles/cdl_inside.py new file mode 100644 index 0000000..6855e75 --- /dev/null +++ b/pandas_ta/candles/cdl_inside.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from pandas import DataFrame, set_option +from pandas_ta.utils import candle_color, get_drift, get_offset +from pandas_ta.utils import non_zero_range, real_body, verify_series + +def cdl_inside(open_, high, low, close, asbool=False, offset=None, **kwargs): + """Candle Type: Inside Bar""" + # Validate arguments + open_ = verify_series(open_) + high = verify_series(high) + low = verify_series(low) + close = verify_series(close) + offset = get_offset(offset) + + # Calculate Result + inside = (high.diff() < 0) & (low.diff() > 0) + + if not asbool: + inside *= candle_color(open_, close) + + # Offset + if offset != 0: + inside = inside.shift(offset) + + # Handle fills + if "fillna" in kwargs: + inside.fillna(kwargs["fillna"], inplace=True) + + if "fill_method" in kwargs: + inside.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + inside.name = f"CDL_INSIDE" + inside.category = "candles" + + return inside + + + +cdl_inside.__doc__ = \ +"""Candle Type: Inside Bar + +An Inside Bar is a bar that is engulfed by the prior highs and lows of it's +previous bar. In other words, the current bar is smaller than it's previous bar. +Set asbool=True if you want to know if it is an Inside Bar. Note by default +asbool=False so this returns a 0 if it is not an Inside Bar, 1 if it is an +Inside Bar and close > open, and -1 if it is an Inside Bar but close < open. + +Sources: + https://www.tradingview.com/script/IyIGN1WO-Inside-Bar/ + +Calculation: + Default Inputs: + asbool=False + inside = (high.diff() < 0) & (low.diff() > 0) + + if not asbool: + inside *= candle_color(open_, close) + +Args: + open_ (pd.Series): Series of 'open's + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + close (pd.Series): Series of 'close's + asbool (bool): Returns the boolean result. 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 +""" diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 927168f..255d7db 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -23,7 +23,7 @@ from pandas_ta.volatility import * from pandas_ta.volume import * from pandas_ta.utils import * -version = ".".join(("0", "2", "01b")) +version = ".".join(("0", "2", "02b")) def mp_worker(args): @@ -610,6 +610,16 @@ class AnalysisIndicators(BasePandasObject): result = cdl_doji(open_=open_, high=high, low=low, close=close, offset=offset, **kwargs) return result + @finalize + def cdl_inside(self, open_=None, high=None, low=None, close=None, offset=None, **kwargs): + open_ = self._get_column(open_, "open") + high = self._get_column(high, "high") + low = self._get_column(low, "low") + close = self._get_column(close, "close") + + result = cdl_inside(open_=open_, high=high, low=low, close=close, offset=offset, **kwargs) + return result + @finalize def ha(self, open_=None, high=None, low=None, close=None, offset=None, **kwargs): open_ = self._get_column(open_, "open") diff --git a/pandas_ta/momentum/inbar.py b/pandas_ta/momentum/inbar.py deleted file mode 100644 index 3b8abd1..0000000 --- a/pandas_ta/momentum/inbar.py +++ /dev/null @@ -1,61 +0,0 @@ -import pandas as pd -from pandas_ta.utils import get_offset , verify_series , zero - -def inbar(self , open , high ,low , close , offset = None , **kwargs ): - """Indicator: Inside Bar""" - # Validate arguments - close = verify_series(close).apply(zero) - open = verify_series(open).apply(zero) - high = verify_series(high).apply(zero) - low = verify_series(low).apply(zero) - offset = get_offset(offset) - prevBar = 1 - - # Calculate Result - bodyStat = (close >= open).rename('bodystat').replace({True: 1 , False:-1}) - isIn = ((high < high.shift(prevBar)) & (low > low.shift(prevBar))).rename('isin') - res = pd.Series(index = close.index , dtype = 'int64') - - for i in close.index: - if isIn[i] == True: - res[i] = bodyStat[i] - else: - res[i] = 0 - - # Offset - if offset != 0: - res = res.shift(offset) - - # Handle fills - if 'fillna' in kwargs: - res.fillna(kwargs['fillna'], inplace=True) - - if 'fill_method' in kwargs: - res.fillna(method=kwargs['fill_method'], inplace=True) - - # Name and Categorize it - res.name = "InBar" - res.category = 'insidebar' - - return res - -inbar.__doc__ = \ -"""Inside Bar -Sources: - https://www.tradingview.com/script/IyIGN1WO-Inside-Bar/ -Calculation: - Default Inputs: - drift=1 - isIn = ((high < high.shift(prevBar)) & (low > low.shift(prevBar))) - bodyStat = (close >= open) -Args: - high (pd.Series): Series of 'high's - low (pd.Series): Series of 'low's - close (pd.Series): Series of 'close's - 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 -""" diff --git a/pandas_ta/overlap/dema.py b/pandas_ta/overlap/dema.py index 266696a..8102aa3 100644 --- a/pandas_ta/overlap/dema.py +++ b/pandas_ta/overlap/dema.py @@ -1,18 +1,17 @@ # -*- coding: utf-8 -*- from .ema import ema -from ..utils import get_offset, verify_series, weights +from pandas_ta.utils import get_offset, verify_series def dema(close, length=None, offset=None, **kwargs): """Indicator: Double Exponential Moving Average (DEMA)""" # 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 offset = get_offset(offset) # Calculate Result - ema1 = ema(close=close, length=length, **kwargs) - ema2 = ema(close=ema1, length=length, **kwargs) + ema1 = ema(close=close, length=length) + ema2 = ema(close=ema1, length=length) dema = 2 * ema1 - ema2 # Offset @@ -21,7 +20,7 @@ def dema(close, length=None, offset=None, **kwargs): # Name & Category dema.name = f"DEMA_{length}" - dema.category = 'overlap' + dema.category = "overlap" return dema diff --git a/pandas_ta/utils.py b/pandas_ta/utils.py index 9bca429..cc3630c 100644 --- a/pandas_ta/utils.py +++ b/pandas_ta/utils.py @@ -3,8 +3,14 @@ import math from pathlib import Path from time import perf_counter -import numpy as np -import pandas as pd +from numpy import argmax, argmin, dot, ones, triu +from numpy import append as npAppend +from numpy import array as npArray +from numpy import ndarray as npNdArray +from numpy import sum as npSum + +from pandas import DataFrame, Series +from pandas.api.types import is_datetime64_any_dtype from functools import reduce from operator import mul @@ -16,8 +22,8 @@ MINUTES_PER_HOUR = 60 def _above_below( - series_a: pd.Series, - series_b: pd.Series, + series_a: Series, + series_b: Series, above: bool = True, asint: bool = True, offset: int = None, @@ -51,8 +57,8 @@ def _above_below( def above( - series_a: pd.Series, - series_b: pd.Series, + series_a: Series, + series_b: Series, asint: bool = True, offset: int = None, **kwargs @@ -61,7 +67,7 @@ def above( def above_value( - series_a: pd.Series, + series_a: Series, value: float, asint: bool = True, offset: int = None, @@ -70,13 +76,13 @@ def above_value( if not isinstance(value, (int, float, complex)): print("[X] value is not a number") return - series_b = pd.Series(value, index=series_a.index, name=f"{value}".replace(".","_")) + series_b = Series(value, index=series_a.index, name=f"{value}".replace(".","_")) return _above_below(series_a, series_b, above=True, asint=asint, offset=offset, **kwargs) def below( - series_a: pd.Series, - series_b: pd.Series, + series_a: Series, + series_b: Series, asint: bool =True, offset: int =None ,**kwargs @@ -85,7 +91,7 @@ def below( def below_value( - series_a: pd.Series, + series_a: Series, value: float, asint: bool = True, offset: int = None, @@ -94,7 +100,7 @@ def below_value( if not isinstance(value, (int, float, complex)): print("[X] value is not a number") return - series_b = pd.Series(value, index=series_a.index, name=f"{value}".replace(".","_")) + series_b = Series(value, index=series_a.index, name=f"{value}".replace(".","_")) return _above_below(series_a, series_b, above=False, asint=asint, offset=offset, **kwargs) @@ -123,20 +129,20 @@ def combination(**kwargs): def cross_value( - series_a: pd.Series, + series_a: Series, value: float, above: bool = True, asint: bool = True, offset: int = None, **kwargs ): - series_b = pd.Series(value, index=series_a.index, name=f"{value}".replace(".","_")) + series_b = Series(value, index=series_a.index, name=f"{value}".replace(".","_")) return cross(series_a, series_b, above, asint, offset, **kwargs) def cross( - series_a: pd.Series, - series_b: pd.Series, + series_a: Series, + series_b: Series, above: bool = True, asint: bool = True, offset: int = None, @@ -169,9 +175,9 @@ def cross( return cross -def is_datetime_ordered(df: pd.DataFrame or pd.Series) -> bool: +def is_datetime_ordered(df: DataFrame or Series) -> bool: """Returns True if the index is a datetime and ordered.""" - index_is_datetime = pd.api.types.is_datetime64_any_dtype(df.index) + index_is_datetime = is_datetime64_any_dtype(df.index) try: ordered = df.index[0] < df.index[-1] except RuntimeWarning: pass @@ -179,8 +185,8 @@ def is_datetime_ordered(df: pd.DataFrame or pd.Series) -> bool: return True if index_is_datetime and ordered else False -def signals(indicator, xa, xb, cross_values, xserie, xserie_a, xserie_b, cross_series, offset) -> pd.DataFrame: - df = pd.DataFrame() +def signals(indicator, xa, xb, cross_values, xserie, xserie_a, xserie_b, cross_series, offset) -> DataFrame: + df = DataFrame() if xa is not None and isinstance(xa, (int, float)): if cross_values: crossed_above_start = cross_value(indicator, xa, above=True, offset=offset) @@ -226,7 +232,7 @@ def signals(indicator, xa, xb, cross_values, xserie, xserie_a, xserie_b, cross_s return df -def df_error_analysis(dfA: pd.DataFrame, dfB: pd.DataFrame, **kwargs) -> pd.DataFrame: +def df_error_analysis(dfA: DataFrame, dfB: DataFrame, **kwargs) -> DataFrame: """DataFrame Correlation Analysis helper""" corr_method = kwargs.pop("corr_method", "pearson") @@ -241,29 +247,29 @@ def df_error_analysis(dfA: pd.DataFrame, dfB: pd.DataFrame, **kwargs) -> pd.Data diff.plot(kind="kde") if kwargs.pop("triangular", False): - return corr.where(np.triu(np.ones(corr.shape)).astype(np.bool)) + return corr.where(triu(ones(corr.shape)).astype(bool)) return corr -def fibonacci(**kwargs) -> np.ndarray: +def fibonacci(n: int = 2, **kwargs) -> npNdArray: """Fibonacci Sequence as a numpy array""" - n = int(math.fabs(kwargs.pop("n", 2))) - zero = kwargs.pop("zero", False) - weighted = kwargs.pop("weighted", False) + n = int(math.fabs(n)) if n >= 0 else 2 + zero = kwargs.pop("zero", False) if zero: a, b = 0, 1 else: n -= 1 a, b = 1, 1 - result = np.array([a]) + result = npArray([a]) for i in range(0, n): a, b = b, a + b - result = np.append(result, a) + result = npAppend(result, a) + weighted = kwargs.pop("weighted", False) if weighted: - fib_sum = np.sum(result) + fib_sum = npSum(result) if fib_sum > 0: return result / fib_sum else: @@ -279,12 +285,12 @@ def final_time(stime): def get_drift(x: int) -> int: """Returns an int if not zero, otherwise defaults to one.""" - return int(x) if x and x != 0 else 1 + return int(x) if isinstance(x, int) and x != 0 else 1 def get_offset(x: int) -> int: """Returns an int, otherwise defaults to zero.""" - return int(x) if x else 0 + return int(x) if isinstance(x, int) else 0 def is_percent(x: int or float) -> bool: @@ -293,9 +299,8 @@ def is_percent(x: int or float) -> bool: return False -def non_zero_range(high: pd.Series, low: pd.Series) -> pd.Series: - """Returns the difference of two series and adds epsilon if - to any zero values. This occurs commonly in crypto data when +def non_zero_range(high: Series, low: Series) -> Series: + """Returns the difference of two series and adds epsilon to any zero values. This occurs commonly in crypto data when high = low. """ diff = high - low @@ -304,7 +309,7 @@ def non_zero_range(high: pd.Series, low: pd.Series) -> pd.Series: return diff -def pascals_triangle(n: int = None, **kwargs) -> np.ndarray: +def pascals_triangle(n: int = None, **kwargs) -> npNdArray: """Pascal's Triangle Returns a numpy array of the nth row of Pascal's Triangle. @@ -313,15 +318,15 @@ def pascals_triangle(n: int = None, **kwargs) -> np.ndarray: => inverse weighted: [0.9375, 0.75, 0.625, 0.75, 0.9375] """ n = int(math.fabs(n)) if n is not None else 0 - weighted = kwargs.pop("weighted", False) - inverse = kwargs.pop("inverse", False) # Calculation - triangle = np.array([combination(n=n, r=i) for i in range(0, n + 1)]) - triangle_sum = np.sum(triangle) + triangle = npArray([combination(n=n, r=i) for i in range(0, n + 1)]) + triangle_sum = npSum(triangle) triangle_weights = triangle / triangle_sum inverse_weights = 1 - triangle_weights + weighted = kwargs.pop("weighted", False) + inverse = kwargs.pop("inverse", False) if weighted and inverse: return inverse_weights if weighted: @@ -333,20 +338,20 @@ def pascals_triangle(n: int = None, **kwargs) -> np.ndarray: def recent_maximum_index(x): - return int(np.argmax(x[::-1])) + return int(argmax(x[::-1])) def recent_minimum_index(x): - return int(np.argmin(x[::-1])) + return int(argmin(x[::-1])) -def signed_series(series: pd.Series, initial: int =None) -> pd.Series: +def signed_series(series: Series, initial: int = None) -> Series: """Returns a Signed Series with or without an initial value Default Example: - series = pd.Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5]) + series = Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5]) and returns: - sign = pd.Series([NaN, -1.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0]) + sign = Series([NaN, -1.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0]) """ series = verify_series(series) sign = series.diff(1) @@ -364,7 +369,6 @@ def symmetric_triangle(n: int = None, **kwargs) -> list: => weighted: [0.16666667 0.33333333 0.33333333 0.16666667] """ n = int(math.fabs(n)) if n is not None else 2 - weighted = kwargs.pop("weighted", False) if n == 2: triangle = [1, 1] @@ -379,24 +383,24 @@ def symmetric_triangle(n: int = None, **kwargs) -> list: front.pop() triangle += front[::-1] - if weighted: - triangle_sum = np.sum(triangle) + if kwargs.pop("weighted", False): + triangle_sum = npSum(triangle) triangle_weights = triangle / triangle_sum return triangle_weights return triangle -def unsigned_differences(series: pd.Series, amount: int = None, **kwargs) -> pd.Series: +def unsigned_differences(series: Series, amount: int = None, **kwargs) -> Series: """Unsigned Differences Returns two Series, an unsigned positive and unsigned negative series based on the differences of the original series. The positive series are only the increases and the negative series is only the decreases. Default Example: - series = pd.Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5, 3]) and returns - postive = pd.Series([0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0]) - negative = pd.Series([0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1]) + series = Series([3, 2, 2, 1, 1, 5, 6, 6, 7, 5, 3]) and returns + postive = Series([0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0]) + negative = Series([0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1]) """ amount = int(amount) if amount is not None else 1 negative = series.diff(amount) @@ -416,15 +420,15 @@ def unsigned_differences(series: pd.Series, amount: int = None, **kwargs) -> pd. return positive, negative -def verify_series(series: pd.Series) -> pd.Series: +def verify_series(series: Series) -> Series: """If a Pandas Series return it.""" - if series is not None and isinstance(series, pd.core.series.Series): + if series is not None and isinstance(series, Series): return series def weights(w): def _dot(x): - return np.dot(w, x) + return dot(w, x) return _dot diff --git a/tests/test_indicator_candle.py b/tests/test_indicator_candle.py index f0a58dc..661d335 100644 --- a/tests/test_indicator_candle.py +++ b/tests/test_indicator_candle.py @@ -53,3 +53,12 @@ class TestCandle(TestCase): self.assertGreater(corr, CORRELATION_THRESHOLD) except Exception as ex: error_analysis(result, CORRELATION, ex) + + def test_cdl_inside(self): + result = pandas_ta.cdl_inside(self.open, self.high, self.low, self.close) + self.assertIsInstance(result, Series) + self.assertEqual(result.name, "CDL_INSIDE") + + result = pandas_ta.cdl_inside(self.open, self.high, self.low, self.close, asbool=True) + self.assertIsInstance(result, Series) + self.assertEqual(result.name, "CDL_INSIDE") \ No newline at end of file diff --git a/tests/test_indicator_candle_ext.py b/tests/test_indicator_candle_ext.py index ebb3035..b447c05 100644 --- a/tests/test_indicator_candle_ext.py +++ b/tests/test_indicator_candle_ext.py @@ -23,12 +23,17 @@ class TestCandleExtension(TestCase): pass - def test_ha_ext(self): - self.data.ta.ha(append=True) - self.assertIsInstance(self.data, DataFrame) - self.assertEqual(list(self.data.columns[-4:]), ["HA_open", "HA_high", "HA_low", "HA_close"]) - def test_cdl_doji_ext(self): self.data.ta.cdl_doji(append=True) self.assertIsInstance(self.data, DataFrame) - self.assertEqual(self.data.columns[-1], "CDL_DOJI_10_0.1") \ No newline at end of file + self.assertEqual(self.data.columns[-1], "CDL_DOJI_10_0.1") + + def test_cdl_inside_ext(self): + self.data.ta.cdl_inside(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], "CDL_INSIDE") + + def test_ha_ext(self): + self.data.ta.ha(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(list(self.data.columns[-4:]), ["HA_open", "HA_high", "HA_low", "HA_close"]) \ No newline at end of file