From 09d22d0266c7df20a7c01564e67148ad69e3e428 Mon Sep 17 00:00:00 2001 From: Cedric Zhuang Date: Thu, 22 Jun 2023 15:14:27 +0800 Subject: [PATCH] [GH-152] Extract Kaufman's Adaptive Moving Average The Efficiency Ratio (ER) is calculated by dividing the price change over a period by the absolute sum of the price movements that occurred to achieve that change. The resulting ratio ranges between 0 and 1 with higher values representing a more efficient or trending market. The default column is close. The default window is 10. Formular: * window_change = ABS(close - close[n]) * last_change = ABS(close-close[1]) * volatility = moving sum of last_change in n * KER = window_change / volatility Examples: * `df['ker']` retrieves the 10 periods KER of the close price * `df['high_5_ker']` retrieves 5 periods KER of the high price --- README.md | 27 ++++++++++++++++++++++ stockstats.py | 63 ++++++++++++++++++++++++++++++++++++++++++--------- test.py | 15 ++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7262bdd..58e8220 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Supported statistics/indicators are: * MFI: Money Flow Index * VWMA: Volume Weighted Moving Average * CHOP: Choppiness Index +* KER: Kaufman's efficiency ratio * KAMA: Kaufman's Adaptive Moving Average * PPO: Percentage Price Oscillator * StochRSI: Stochastic RSI @@ -669,6 +670,32 @@ Examples: * `df['mfi']` retrieves the 14 periods MFI * `df['mfi_6']` retrieves the 6 periods MFI + +#### [KER - Kaufman's efficiency ratio](https://strategyquant.com/codebase/kaufmans-efficiency-ratio-ker/) + +The Efficiency Ratio (ER) is calculated by +dividing the price change over a period by the +absolute sum of the price movements that occurred +to achieve that change. + +The resulting ratio ranges between 0 and 1 with +higher values representing a more efficient or +trending market. + +The default column is close. + +The default window is 10. + +Formular: +* window_change = ABS(close - close[n]) +* last_change = ABS(close-close[1]) +* volatility = moving sum of last_change in n +* KER = window_change / volatility + +Examples: +* `df['ker']` retrieves the 10 periods KER of the close price +* `df['high_5_ker']` retrieves 5 periods KER of the high price + #### [KAMA - Kaufman's Adaptive Moving Average](https://school.stockcharts.com/doku.php?id=technical_indicators:kaufman_s_adaptive_moving_average) Kaufman's Adaptive Moving Average is designed to account for market noise or diff --git a/stockstats.py b/stockstats.py index 607d5e0..9c85c1e 100644 --- a/stockstats.py +++ b/stockstats.py @@ -106,6 +106,8 @@ class StockDataFrame(pd.DataFrame): WAVE_TREND_1 = 10 WAVE_TREND_2 = 21 + ER = 10 + KAMA_SLOW = 34 KAMA_FAST = 5 @@ -724,14 +726,14 @@ def _get_um_dm(self): initialize up move and down move """ - hd = self._col_delta('high') + hd = self._col_diff('high') self['um'] = (hd > 0) * hd - ld = -self._col_delta('low') + ld = -self._col_diff('low') self['dm'] = (ld > 0) * ld def _get_pdm_ndm(self, window): - hd = self._col_delta('high') - ld = -self._col_delta('low') + hd = self._col_diff('high') + ld = -self._col_diff('low') p = ((hd > 0) & (hd > ld)) * hd n = ((ld > 0) & (ld > hd)) * ld if window > 1: @@ -1495,6 +1497,48 @@ def _get_cmo(self, window=None): res.iloc[0] = 0 self[column_name] = res + def ker(self, column, window): + col = self[column] + col_window_s = self._shift(col, -window) + window_diff = (col - col_window_s).abs() + diff = self._col_diff(column).abs() + volatility = self.mov_sum(diff, window) + ret = window_diff / volatility + ret.iloc[0] = 0 + return ret + + def _get_ker(self, column=None, window=None): + """ get Kaufman's efficiency ratio + + The Efficiency Ratio (ER) is calculated by + dividing the price change over a period by the + absolute sum of the price movements that occurred + to achieve that change. + The resulting ratio ranges between 0 and 1 with + higher values representing a more efficient or + trending market. + + The default column is close. + The default window is 10. + + https://strategyquant.com/codebase/kaufmans-efficiency-ratio-ker/ + + Formular: + window_change = ABS(close - close[n]) + last_change = ABS(close-close[1]) + volatility = moving sum of last_change in n + KER = window_change / volatility + """ + if column is None and window is None: + column = 'close' + window = self.ER + column_name = 'ker' + else: + window = self.get_int_positive(window) + column_name = '{}_{}_ker'.format(column, window) + + self[column_name] = self.ker(column, window) + def _get_kama(self, column, windows, fasts=None, slows=None): """ get Kaufman's Adaptive Moving Average. Implemented after @@ -1516,11 +1560,7 @@ def _get_kama(self, column, windows, fasts=None, slows=None): column_name = '{}_{}_kama_{}_{}'.format(column, window, fast, slow) col = self[column] - col_window_s = self._shift(col, -window) - col_last = self._shift(col, -1) - change = (col - col_window_s).abs() - volatility = self.mov_sum((col - col_last).abs(), window) - efficiency_ratio = change / volatility + efficiency_ratio = self.ker(column, window) fast_ema_smoothing = 2.0 / (fast + 1) slow_ema_smoothing = 2.0 / (slow + 1) smoothing_2 = fast_ema_smoothing - slow_ema_smoothing @@ -1578,14 +1618,14 @@ def _get_rate(self): """ self['rate'] = self['close'].pct_change() * 100 - def _col_delta(self, col): + def _col_diff(self, col): ret = self[col].diff() ret.iloc[0] = 0.0 return ret def _get_delta(self, key): key_to_delta = key.replace('_delta', '') - self[key] = self._col_delta(key_to_delta) + self[key] = self._col_diff(key_to_delta) return self[key] def _get_cross(self, key): @@ -1683,6 +1723,7 @@ def handler(self): ('coppock',): self._get_coppock, ('ichimoku',): self._get_ichimoku, ('cti',): self._get_cti, + ('ker',): self._get_ker, } def __init_not_exist_column(self, key): diff --git a/test.py b/test.py index 959e129..0b06452 100644 --- a/test.py +++ b/test.py @@ -617,6 +617,21 @@ def test_mfi(self): assert_that(mfi_15.loc[first], near_to(0.5)) assert_that(mfi_15.loc[last], near_to(0.6733)) + def test_ker(self): + stock = self.get_stock_90days() + k = stock['ker'] + assert_that(k[20110104], equal_to(0)) + assert_that(k[20110105], equal_to(1)) + assert_that(k[20110210], near_to(0.305)) + + k = stock['close_10_ker'] + assert_that(k[20110104], equal_to(0)) + assert_that(k[20110105], equal_to(1)) + assert_that(k[20110210], near_to(0.305)) + + k = stock['high_5_ker'] + assert_that(k[20110210], near_to(0.399)) + def test_column_kama(self): stock = self.get_stock_90days() idx = 20110331