From 6669416e26a7dbbbb34972b237ffa27d135101a7 Mon Sep 17 00:00:00 2001 From: Bukosabino Date: Sat, 21 Mar 2020 14:05:27 +0100 Subject: [PATCH 1/5] volume indicators isolation: https://github.com/bukosabino/ta/issues/129 --- ta/momentum.py | 90 ------------------------------------------ ta/tests/__init__.py | 10 ++--- ta/tests/momentum.py | 26 +------------ ta/tests/volume.py | 36 ++++++++++++++++- ta/volume.py | 93 ++++++++++++++++++++++++++++++++++++++++++++ ta/wrapper.py | 17 ++++---- 6 files changed, 142 insertions(+), 130 deletions(-) diff --git a/ta/momentum.py b/ta/momentum.py index 7c74f822..f8031a64 100644 --- a/ta/momentum.py +++ b/ta/momentum.py @@ -51,69 +51,6 @@ def rsi(self) -> pd.Series: return pd.Series(rsi, name='rsi') -class MFIIndicator(IndicatorMixin): - """Money Flow Index (MFI) - - Uses both price and volume to measure buying and selling pressure. It is - positive when the typical price rises (buying pressure) and negative when - the typical price declines (selling pressure). A ratio of positive and - negative money flow is then plugged into an RSI formula to create an - oscillator that moves between zero and one hundred. - - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:money_flow_index_mfi - - Args: - high(pandas.Series): dataset 'High' column. - low(pandas.Series): dataset 'Low' column. - close(pandas.Series): dataset 'Close' column. - volume(pandas.Series): dataset 'Volume' column. - n(int): n period. - fillna(bool): if True, fill nan values. - """ - - def __init__(self, - high: pd.Series, - low: pd.Series, - close: pd.Series, - volume: pd.Series, - n: int = 14, - fillna: bool = False): - self._high = high - self._low = low - self._close = close - self._volume = volume - self._n = n - self._fillna = fillna - self._run() - - def _run(self): - # 1 typical price - tp = (self._high + self._low + self._close) / 3.0 - - # 2 up or down column - up_down = np.where(tp > tp.shift(1), 1, np.where(tp < tp.shift(1), -1, 0)) - - # 3 money flow - mf = tp * self._volume * up_down - - # 4 positive and negative money flow with n periods - n_positive_mf = mf.rolling(self._n).apply(lambda x: np.sum(np.where(x >= 0.0, x, 0.0)), raw=True) - n_negative_mf = abs(mf.rolling(self._n).apply(lambda x: np.sum(np.where(x < 0.0, x, 0.0)), raw=True)) - - # 5 money flow index - mr = n_positive_mf / n_negative_mf - self._mr = (100 - (100 / (1 + mr))) - - def money_flow_index(self) -> pd.Series: - """Money Flow Index (MFI) - - Returns: - pandas.Series: New feature generated. - """ - mr = self._check_fillna(self._mr, value=50) - return pd.Series(mr, name=f'mfi_{self._n}') - - class TSIIndicator(IndicatorMixin): """True strength index (TSI) @@ -530,33 +467,6 @@ def rsi(close, n=14, fillna=False): return RSIIndicator(close=close, n=n, fillna=fillna).rsi() -def money_flow_index(high, low, close, volume, n=14, fillna=False): - """Money Flow Index (MFI) - - Uses both price and volume to measure buying and selling pressure. It is - positive when the typical price rises (buying pressure) and negative when - the typical price declines (selling pressure). A ratio of positive and - negative money flow is then plugged into an RSI formula to create an - oscillator that moves between zero and one hundred. - - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:money_flow_index_mfi - - Args: - high(pandas.Series): dataset 'High' column. - low(pandas.Series): dataset 'Low' column. - close(pandas.Series): dataset 'Close' column. - volume(pandas.Series): dataset 'Volume' column. - n(int): n period. - fillna(bool): if True, fill nan values. - - Returns: - pandas.Series: New feature generated. - - """ - indicator = MFIIndicator(high=high, low=low, close=close, volume=volume, n=n, fillna=fillna) - return indicator.money_flow_index() - - def tsi(close, r=25, s=13, fillna=False): """True strength index (TSI) diff --git a/ta/tests/__init__.py b/ta/tests/__init__.py index 7667d4d4..1c46a948 100644 --- a/ta/tests/__init__.py +++ b/ta/tests/__init__.py @@ -1,7 +1,7 @@ -from ta.tests.momentum import (TestKAMAIndicator, TestMFIIndicator, - TestRateOfChangeIndicator, TestRSIIndicator, - TestStochasticOscillator, TestTSIIndicator, - TestUltimateOscillator, TestWilliamsRIndicator) +from ta.tests.momentum import (TestKAMAIndicator, TestRateOfChangeIndicator, + TestRSIIndicator, TestStochasticOscillator, + TestTSIIndicator, TestUltimateOscillator, + TestWilliamsRIndicator) from ta.tests.trend import (TestADXIndicator, TestCCIIndicator, TestMACDIndicator, TestPSARIndicator, TestVortexIndicator) @@ -9,5 +9,5 @@ from ta.tests.volatility import TestAverageTrueRange, TestBollingerBands from ta.tests.volume import (TestAccDistIndexIndicator, TestEaseOfMovementIndicator, - TestForceIndexIndicator, + TestForceIndexIndicator, TestMFIIndicator, TestOnBalanceVolumeIndicator) diff --git a/ta/tests/momentum.py b/ta/tests/momentum.py index 0a3fe617..2b7deb7e 100644 --- a/ta/tests/momentum.py +++ b/ta/tests/momentum.py @@ -2,8 +2,8 @@ import pandas as pd -from ta.momentum import (KAMAIndicator, MFIIndicator, ROCIndicator, - RSIIndicator, StochasticOscillator, TSIIndicator, +from ta.momentum import (KAMAIndicator, ROCIndicator, RSIIndicator, + StochasticOscillator, TSIIndicator, UltimateOscillator, WilliamsRIndicator, roc) from ta.tests.utils import TestIndicator @@ -47,28 +47,6 @@ def test_rsi(self): pd.testing.assert_series_equal(self._df[target].tail(), result.tail(), check_names=False) -class TestMFIIndicator(unittest.TestCase): - """ - https://school.stockcharts.com/doku.php?id=technical_indicators:money_flow_index_mfi - """ - - _filename = 'ta/tests/data/cs-mfi.csv' - - def setUp(self): - self._df = pd.read_csv(self._filename, sep=',') - self._indicator = MFIIndicator( - high=self._df['High'], low=self._df['Low'], close=self._df['Close'], volume=self._df['Volume'], n=14, - fillna=False) - - def tearDown(self): - del(self._df) - - def test_mfi(self): - target = 'MFI' - result = self._indicator.money_flow_index() - pd.testing.assert_series_equal(self._df[target].tail(), result.tail(), check_names=False) - - class TestUltimateOscillator(unittest.TestCase): """ https://school.stockcharts.com/doku.php?id=technical_indicators:ultimate_oscillator diff --git a/ta/tests/volume.py b/ta/tests/volume.py index 34fff352..63d775be 100644 --- a/ta/tests/volume.py +++ b/ta/tests/volume.py @@ -1,9 +1,12 @@ +import unittest + import pandas as pd from ta.tests.utils import TestIndicator from ta.volume import (AccDistIndexIndicator, EaseOfMovementIndicator, - ForceIndexIndicator, OnBalanceVolumeIndicator, - acc_dist_index, ease_of_movement, force_index, + ForceIndexIndicator, MFIIndicator, + OnBalanceVolumeIndicator, acc_dist_index, + ease_of_movement, force_index, money_flow_index, on_balance_volume, sma_ease_of_movement) @@ -99,3 +102,32 @@ def test_adl2(self): high=self._df['High'], low=self._df['Low'], close=self._df['Close'], volume=self._df['Volume'], fillna=False).acc_dist_index() pd.testing.assert_series_equal(self._df[target].tail(), result.tail(), check_names=False) + + +class TestMFIIndicator(unittest.TestCase): + """ + https://school.stockcharts.com/doku.php?id=technical_indicators:money_flow_index_mfi + """ + + _filename = 'ta/tests/data/cs-mfi.csv' + + def setUp(self): + self._df = pd.read_csv(self._filename, sep=',') + self._indicator = MFIIndicator( + high=self._df['High'], low=self._df['Low'], close=self._df['Close'], volume=self._df['Volume'], n=14, + fillna=False) + + def tearDown(self): + del(self._df) + + def test_mfi(self): + target = 'MFI' + result = self._indicator.money_flow_index() + pd.testing.assert_series_equal(self._df[target].tail(), result.tail(), check_names=False) + + def test_mfi2(self): + target = 'MFI' + result = money_flow_index( + high=self._df['High'], low=self._df['Low'], close=self._df['Close'], volume=self._df['Volume'], n=14, + fillna=False) + pd.testing.assert_series_equal(self._df[target].tail(), result.tail(), check_names=False) diff --git a/ta/volume.py b/ta/volume.py index f70d476f..2eb42092 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -289,6 +289,72 @@ def negative_volume_index(self) -> pd.Series: return pd.Series(nvi, name='nvi') +class MFIIndicator(IndicatorMixin): + """Money Flow Index (MFI) + + Uses both price and volume to measure buying and selling pressure. It is + positive when the typical price rises (buying pressure) and negative when + the typical price declines (selling pressure). A ratio of positive and + negative money flow is then plugged into an RSI formula to create an + oscillator that moves between zero and one hundred. + + http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:money_flow_index_mfi + + Args: + high(pandas.Series): dataset 'High' column. + low(pandas.Series): dataset 'Low' column. + close(pandas.Series): dataset 'Close' column. + volume(pandas.Series): dataset 'Volume' column. + n(int): n period. + fillna(bool): if True, fill nan values. + """ + + def __init__(self, + high: pd.Series, + low: pd.Series, + close: pd.Series, + volume: pd.Series, + n: int = 14, + fillna: bool = False): + self._high = high + self._low = low + self._close = close + self._volume = volume + self._n = n + self._fillna = fillna + self._run() + + def _run(self): + # 1 typical price + tp = (self._high + self._low + self._close) / 3.0 + + # 2 up or down column + up_down = np.where(tp > tp.shift(1), 1, np.where(tp < tp.shift(1), -1, 0)) + + # 3 money flow + mf = tp * self._volume * up_down + + # 4 positive and negative money flow with n periods + n_positive_mf = mf.rolling(self._n).apply(lambda x: np.sum(np.where(x >= 0.0, x, 0.0)), raw=True) + n_negative_mf = abs(mf.rolling(self._n).apply(lambda x: np.sum(np.where(x < 0.0, x, 0.0)), raw=True)) + + # n_positive_mf = np.where(mf.rolling(self._n).sum() >= 0.0, mf, 0.0) + # n_negative_mf = abs(np.where(mf.rolling(self._n).sum() < 0.0, mf, 0.0)) + + # 5 money flow index + mr = n_positive_mf / n_negative_mf + self._mr = (100 - (100 / (1 + mr))) + + def money_flow_index(self) -> pd.Series: + """Money Flow Index (MFI) + + Returns: + pandas.Series: New feature generated. + """ + mr = self._check_fillna(self._mr, value=50) + return pd.Series(mr, name=f'mfi_{self._n}') + + def acc_dist_index(high, low, close, volume, fillna=False): """Accumulation/Distribution Index (ADI) @@ -478,6 +544,33 @@ def negative_volume_index(close, volume, fillna=False): return NegativeVolumeIndexIndicator(close=close, volume=volume, fillna=fillna).negative_volume_index() +def money_flow_index(high, low, close, volume, n=14, fillna=False): + """Money Flow Index (MFI) + + Uses both price and volume to measure buying and selling pressure. It is + positive when the typical price rises (buying pressure) and negative when + the typical price declines (selling pressure). A ratio of positive and + negative money flow is then plugged into an RSI formula to create an + oscillator that moves between zero and one hundred. + + http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:money_flow_index_mfi + + Args: + high(pandas.Series): dataset 'High' column. + low(pandas.Series): dataset 'Low' column. + close(pandas.Series): dataset 'Close' column. + volume(pandas.Series): dataset 'Volume' column. + n(int): n period. + fillna(bool): if True, fill nan values. + + Returns: + pandas.Series: New feature generated. + + """ + indicator = MFIIndicator(high=high, low=low, close=close, volume=volume, n=n, fillna=fillna) + return indicator.money_flow_index() + + # TODO def put_call_ratio(): """Put/Call ratio (PCR) diff --git a/ta/wrapper.py b/ta/wrapper.py index da983f45..0b67e147 100644 --- a/ta/wrapper.py +++ b/ta/wrapper.py @@ -8,9 +8,8 @@ import pandas as pd from ta.momentum import (AwesomeOscillatorIndicator, KAMAIndicator, - MFIIndicator, ROCIndicator, RSIIndicator, - StochasticOscillator, TSIIndicator, - UltimateOscillator, WilliamsRIndicator) + ROCIndicator, RSIIndicator, StochasticOscillator, + TSIIndicator, UltimateOscillator, WilliamsRIndicator) from ta.others import (CumulativeReturnIndicator, DailyLogReturnIndicator, DailyReturnIndicator) from ta.trend import (MACD, ADXIndicator, AroonIndicator, CCIIndicator, @@ -21,8 +20,8 @@ KeltnerChannel) from ta.volume import (AccDistIndexIndicator, ChaikinMoneyFlowIndicator, EaseOfMovementIndicator, ForceIndexIndicator, - NegativeVolumeIndexIndicator, OnBalanceVolumeIndicator, - VolumePriceTrendIndicator) + MFIIndicator, NegativeVolumeIndexIndicator, + OnBalanceVolumeIndicator, VolumePriceTrendIndicator) def add_volume_ta(df: pd.DataFrame, high: str, low: str, close: str, volume: str, @@ -58,6 +57,10 @@ def add_volume_ta(df: pd.DataFrame, high: str, low: str, close: str, volume: str df[f'{colprefix}volume_fi'] = ForceIndexIndicator( close=df[close], volume=df[volume], n=13, fillna=fillna).force_index() + # Money Flow Indicator + df[f'{colprefix}momentum_mfi'] = MFIIndicator( + high=df[high], low=df[low], close=df[close], volume=df[volume], n=14, fillna=fillna).money_flow_index() + # Ease of Movement indicator = EaseOfMovementIndicator(high=df[high], low=df[low], volume=df[volume], n=14, fillna=fillna) df[f'{colprefix}volume_em'] = indicator.ease_of_movement() @@ -231,10 +234,6 @@ def add_momentum_ta(df: pd.DataFrame, high: str, low: str, close: str, volume: s # Relative Strength Index (RSI) df[f'{colprefix}momentum_rsi'] = RSIIndicator(close=df[close], n=14, fillna=fillna).rsi() - # Money Flow Indicator - df[f'{colprefix}momentum_mfi'] = MFIIndicator( - high=df[high], low=df[low], close=df[close], volume=df[volume], n=14, fillna=fillna).money_flow_index() - # TSI Indicator df[f'{colprefix}momentum_tsi'] = TSIIndicator(close=df[close], r=25, s=13, fillna=fillna).tsi() From 8e6a3ca4cb17b6fc9716760a64c1ee237b0cd7fa Mon Sep 17 00:00:00 2001 From: Bukosabino Date: Sat, 21 Mar 2020 17:32:31 +0100 Subject: [PATCH 2/5] fixing warning (python3.8) --- ta/trend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ta/trend.py b/ta/trend.py index 6ff3d224..13a4a301 100644 --- a/ta/trend.py +++ b/ta/trend.py @@ -496,7 +496,7 @@ def __init__(self, high: pd.Series, low: pd.Series, close: pd.Series, n: int = 1 self._run() def _run(self): - assert self._n is not 0, "N may not be 0 and is %r" % n + assert self._n != 0, "N may not be 0 and is %r" % n cs = self._close.shift(1) pdm = get_min_max(self._high, cs, 'max') From 3e3fe07ab6bb901a66b62a12371acffc314ad689 Mon Sep 17 00:00:00 2001 From: Bukosabino Date: Sat, 21 Mar 2020 17:33:31 +0100 Subject: [PATCH 3/5] fixing warning view/copy: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy --- ta/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ta/utils.py b/ta/utils.py index 2a2b3ed6..5c7eb867 100644 --- a/ta/utils.py +++ b/ta/utils.py @@ -30,6 +30,7 @@ def _check_fillna(self, serie: pd.Series, value: int = 0): def dropna(df): """Drop rows with "Nans" values """ + df = df.copy() number_cols = df.select_dtypes('number').columns.to_list() df[number_cols] = df[number_cols][df[number_cols] < math.exp(709)] # big number df[number_cols] = df[number_cols][df[number_cols] != 0.0] From 0ec2fd9c46ddb14d1d40b4209a5092b7ea5d5af7 Mon Sep 17 00:00:00 2001 From: Bukosabino Date: Sat, 21 Mar 2020 17:40:24 +0100 Subject: [PATCH 4/5] fixing bug: testing with dropna dataframes --- ta/tests/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ta/tests/utils.py b/ta/tests/utils.py index b5f00670..4ed416c9 100644 --- a/ta/tests/utils.py +++ b/ta/tests/utils.py @@ -23,7 +23,7 @@ def test_general(self): df = ta.utils.dropna(self._df) # Add all ta features filling nans values - ta.add_all_ta_features(self._df, "Open", "High", "Low", "Close", "Volume_BTC", fillna=True) + ta.add_all_ta_features(df, "Open", "High", "Low", "Close", "Volume_BTC", fillna=True) # Add all ta features not filling nans values - df = ta.add_all_ta_features(self._df, "Open", "High", "Low", "Close", "Volume_BTC", fillna=False) + df = ta.add_all_ta_features(df, "Open", "High", "Low", "Close", "Volume_BTC", fillna=False) From 5438c949518274d0f6957480ac0f97f735c0cd98 Mon Sep 17 00:00:00 2001 From: Bukosabino Date: Sat, 21 Mar 2020 17:42:44 +0100 Subject: [PATCH 5/5] fixing bug: keltner channel high band calculation. Also, included percentage band and width band indicators for keltner channel. Also, included 2 ways to calculate the centerline of Keltner Channel: original version (sma of typical price) vs modern version (ema of close) --- ta/volatility.py | 56 ++++++++++++++++++++++++++++++++++++++---------- ta/wrapper.py | 4 +++- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/ta/volatility.py b/ta/volatility.py index 93c4a21c..6b8ca3f6 100644 --- a/ta/volatility.py +++ b/ta/volatility.py @@ -171,24 +171,38 @@ class KeltnerChannel(IndicatorMixin): close(pandas.Series): dataset 'Close' column. n(int): n period. fillna(bool): if True, fill nan values. + ov(bool): if True, use original version as the centerline (SMA of typical price) + if False, use EMA of close as the centerline. More info: + https://school.stockcharts.com/doku.php?id=technical_indicators:keltner_channels """ - def __init__(self, high: pd.Series, low: pd.Series, close: pd.Series, n: int = 14, fillna: bool = False): + def __init__( + self, high: pd.Series, low: pd.Series, close: pd.Series, n: int = 14, fillna: bool = False, + ov: bool = True): self._high = high self._low = low self._close = close self._n = n self._fillna = fillna + self._ov = ov self._run() def _run(self): - self._tp = ((self._high + self._low + self._close) / 3.0).rolling(self._n, min_periods=0).mean() - self._tp_high = (((4 * self._high) - (2 * self._low) + self._close) / 3.0).rolling( - self._n, min_periods=0).mean() - self._tp_low = (((-2 * self._high) + (4 * self._low) + self._close) / 3.0).rolling( - self._n, min_periods=0).mean() - - def keltner_channel_central(self) -> pd.Series: + if self._ov: + self._tp = ((self._high + self._low + self._close) / 3.0).rolling(self._n, min_periods=0).mean() + self._tp_high = (((4 * self._high) - (2 * self._low) + self._close) / 3.0).rolling( + self._n, min_periods=0).mean() + self._tp_low = (((-2 * self._high) + (4 * self._low) + self._close) / 3.0).rolling( + self._n, min_periods=0).mean() + else: + self._tp = self._close.ewm(span=self._n, min_periods=0, adjust=False).mean() + atr = AverageTrueRange( + close=self._close, high=self._high, low=self._high, n=10, fillna=self._fillna + ).average_true_range() + self._tp_high = self._tp + (2*atr) + self._tp_low = self._tp - (2*atr) + + def keltner_channel_mband(self) -> pd.Series: """Keltner Channel Middle Band Returns: @@ -203,7 +217,7 @@ def keltner_channel_hband(self) -> pd.Series: Returns: pandas.Series: New feature generated. """ - tp = self._check_fillna(self._tp, value=-1) + tp = self._check_fillna(self._tp_high, value=-1) return pd.Series(tp, name='kc_hband') def keltner_channel_lband(self) -> pd.Series: @@ -215,6 +229,26 @@ def keltner_channel_lband(self) -> pd.Series: tp_low = self._check_fillna(self._tp_low, value=-1) return pd.Series(tp_low, name='kc_lband') + def keltner_channel_wband(self) -> pd.Series: + """Keltner Channel Band Width + + Returns: + pandas.Series: New feature generated. + """ + wband = ((self._tp_high - self._tp_low) / self._tp) * 100 + wband = self._check_fillna(wband, value=0) + return pd.Series(wband, name='bbiwband') + + def keltner_channel_pband(self) -> pd.Series: + """Keltner Channel Percentage Band + + Returns: + pandas.Series: New feature generated. + """ + pband = (self._close - self._tp_low) / (self._tp_high - self._tp_low) + pband = self._check_fillna(pband, value=0) + return pd.Series(pband, name='bbipband') + def keltner_channel_hband_indicator(self) -> pd.Series: """Keltner Channel Indicator Crossing High Band (binary) @@ -429,7 +463,7 @@ def bollinger_lband_indicator(close, n=20, ndev=2, fillna=False): return indicator.bollinger_hband_indicator() -def keltner_channel_central(high, low, close, n=10, fillna=False): +def keltner_channel_mband(high, low, close, n=10, fillna=False): """Keltner channel (KC) Showing a simple moving average line (central) of typical price. @@ -447,7 +481,7 @@ def keltner_channel_central(high, low, close, n=10, fillna=False): pandas.Series: New feature generated. """ indicator = KeltnerChannel(high=high, low=low, close=close, n=n, fillna=False) - return indicator.keltner_channel_central() + return indicator.keltner_channel_mband() def keltner_channel_hband(high, low, close, n=10, fillna=False): diff --git a/ta/wrapper.py b/ta/wrapper.py index 0b67e147..22b35db2 100644 --- a/ta/wrapper.py +++ b/ta/wrapper.py @@ -109,9 +109,11 @@ def add_volatility_ta(df: pd.DataFrame, high: str, low: str, close: str, # Keltner Channel indicator_kc = KeltnerChannel(close=df[close], high=df[high], low=df[low], n=10, fillna=fillna) - df[f'{colprefix}volatility_kcc'] = indicator_kc.keltner_channel_central() + df[f'{colprefix}volatility_kcc'] = indicator_kc.keltner_channel_mband() df[f'{colprefix}volatility_kch'] = indicator_kc.keltner_channel_hband() df[f'{colprefix}volatility_kcl'] = indicator_kc.keltner_channel_lband() + df[f'{colprefix}volatility_kcw'] = indicator_kc.keltner_channel_wband() + df[f'{colprefix}volatility_kcp'] = indicator_kc.keltner_channel_pband() df[f'{colprefix}volatility_kchi'] = indicator_kc.keltner_channel_hband_indicator() df[f'{colprefix}volatility_kcli'] = indicator_kc.keltner_channel_lband_indicator()