ENH #117 #109 inside bar added MAINT utils refactoring

This commit is contained in:
Kevin Johnson
2020-09-08 11:34:29 -07:00
parent cdb4960a1d
commit b5ab65cbea
10 changed files with 174 additions and 130 deletions
+4 -1
View File
@@ -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)
+1 -1
View File
@@ -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"],
+2 -1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from .ha import ha
from .cdl_doji import cdl_doji
from .cdl_doji import cdl_doji
from .cdl_inside import cdl_inside
+74
View File
@@ -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
"""
+11 -1
View File
@@ -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")
-61
View File
@@ -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
"""
+4 -5
View File
@@ -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
+58 -54
View File
@@ -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
+9
View File
@@ -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")
+11 -6
View File
@@ -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")
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"])