From d9e836ac52e4f9c266e84c8a86d096b086d2d41a Mon Sep 17 00:00:00 2001 From: Cedric Zhuang Date: Thu, 22 Jun 2023 16:15:46 +0800 Subject: [PATCH] [GH-150] Fix pdm and ndm The directional movement index should be calculated with smoothed moving average. Also update the calculation so that we don't generate extra columns. Drop support for python 2.7 --- .github/workflows/build-test.yml | 2 +- setup.py | 2 +- stockstats.py | 100 ++++++++++++++++++------------- test.py | 41 ++++++++----- 4 files changed, 86 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6ab4a7d..cf6f119 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "2.7", "3.10", "3.11" ] + python-version: [ "3.9", "3.10", "3.11" ] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/setup.py b/setup.py index ba6a7c5..fa4b861 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ def get_long_description(): long_description=get_long_description(), classifiers=[ "Programming Language :: Python", - "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", diff --git a/stockstats.py b/stockstats.py index 5ef64bb..607d5e0 100644 --- a/stockstats.py +++ b/stockstats.py @@ -343,8 +343,8 @@ def _get_rsi(self, window=None): change = self._delta(self['close'], -1) close_pm = (change + change.abs()) / 2 close_nm = (-change + change.abs()) / 2 - p_ema = self._smma(close_pm, window) - n_ema = self._smma(close_nm, window) + p_ema = self.smma(close_pm, window) + n_ema = self.smma(close_nm, window) rs_column_name = 'rs_{}'.format(window) self[rs_column_name] = rs = p_ema / n_ema @@ -395,7 +395,7 @@ def _get_wave_trend(self): self["wt2"] = self.sma(tci, 4) @staticmethod - def _smma(series, window): + def smma(series, window): return series.ewm( ignore_na=False, alpha=1.0 / window, @@ -411,7 +411,7 @@ def _get_smma(self, column, windows): """ window = self.get_int_positive(windows) column_name = '{}_{}_smma'.format(column, window) - self[column_name] = self._smma(self[column], window) + self[column_name] = self.smma(self[column], window) def _get_trix(self, column=None, windows=None): """ Triple Exponential Average @@ -673,7 +673,7 @@ def _get_z(self, column, window): def _atr(self, window): tr = self._tr() - return self._smma(tr, window) + return self.smma(tr, window) def _get_atr(self, window=None): """ Average True Range @@ -725,9 +725,27 @@ def _get_um_dm(self): initialize up move and down move """ hd = self._col_delta('high') - self['um'] = (hd + hd.abs()) / 2 + self['um'] = (hd > 0) * hd ld = -self._col_delta('low') - self['dm'] = (ld + ld.abs()) / 2 + self['dm'] = (ld > 0) * ld + + def _get_pdm_ndm(self, window): + hd = self._col_delta('high') + ld = -self._col_delta('low') + p = ((hd > 0) & (hd > ld)) * hd + n = ((ld > 0) & (ld > hd)) * ld + if window > 1: + p = self.smma(p, window) + n = self.smma(n, window) + return p, n + + def _pdm(self, window): + ret, _ = self._get_pdm_ndm(window) + return ret + + def _ndm(self, window): + _, ret = self._get_pdm_ndm(window) + return ret def _get_pdm(self, windows): """ +DM, positive directional moving @@ -738,14 +756,26 @@ def _get_pdm(self, windows): :return: """ window = self.get_int_positive(windows) - column_name = 'pdm_{}'.format(window) - um, dm = self['um'], self['dm'] - self['pdm'] = np.where(um > dm, um, 0) if window > 1: - pdm = self['pdm_{}_ema'.format(window)] + column_name = 'pdm_{}'.format(window) + else: + column_name = 'pdm' + self[column_name] = self._pdm(window) + + def _get_ndm(self, windows): + """ -DM, negative directional moving accumulation + + If window is not 1, return the SMA of -DM. + + :param windows: range + :return: + """ + window = self.get_int_positive(windows) + if window > 1: + column_name = 'ndm_{}'.format(window) else: - pdm = self['pdm'] - self[column_name] = pdm + column_name = 'ndm' + self[column_name] = self._ndm(window) def _get_vr(self, windows=None): if windows is None: @@ -770,23 +800,12 @@ def _get_vr(self, windows=None): self[column_name] = (avs + cvs / 2) / (bvs + cvs / 2) * 100 - def _get_mdm(self, windows): - """ -DM, negative directional moving accumulation - - If window is not 1, return the SMA of -DM. - - :param windows: range - :return: - """ - window = self.get_int_positive(windows) - column_name = 'mdm_{}'.format(window) - um, dm = self['um'], self['dm'] - self['mdm'] = np.where(dm > um, dm, 0) - if window > 1: - mdm = self['mdm_{}_ema'.format(window)] - else: - mdm = self['mdm'] - self[column_name] = mdm + def _get_pdi_ndi(self, window): + pdm, ndm = self._get_pdm_ndm(window) + atr = self._atr(window) + pdi = pdm / atr * 100 + ndi = ndm / atr * 100 + return pdi, ndi def _get_pdi(self, windows): """ +DI, positive directional moving index @@ -795,26 +814,22 @@ def _get_pdi(self, windows): :return: """ window = self.get_int_positive(windows) - pdm_column = 'pdm_{}'.format(window) - tr_column = 'atr_{}'.format(window) + pdi, _ = self._get_pdi_ndi(window) pdi_column = 'pdi_{}'.format(window) - self[pdi_column] = self[pdm_column] / self[tr_column] * 100 + self[pdi_column] = pdi return self[pdi_column] def _get_mdi(self, windows): window = self.get_int_positive(windows) - mdm_column = 'mdm_{}'.format(window) - tr_column = 'atr_{}'.format(window) + _, ndi = self._get_pdi_ndi(window) mdi_column = 'mdi_{}'.format(window) - self[mdi_column] = self[mdm_column] / self[tr_column] * 100 + self[mdi_column] = ndi return self[mdi_column] def _get_dx(self, windows): window = self.get_int_positive(windows) dx_column = 'dx_{}'.format(window) - mdi_column = 'mdi_{}'.format(window) - pdi_column = 'pdi_{}'.format(window) - mdi, pdi = self[mdi_column], self[pdi_column] + pdi, mdi = self._get_pdi_ndi(window) self[dx_column] = abs(pdi - mdi) / (pdi + mdi) * 100 return self[dx_column] @@ -1564,11 +1579,13 @@ def _get_rate(self): self['rate'] = self['close'].pct_change() * 100 def _col_delta(self, col): - return self[col].diff() + 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[key_to_delta].diff() + self[key] = self._col_delta(key_to_delta) return self[key] def _get_cross(self, key): @@ -1645,7 +1662,6 @@ def handler(self): ('cci',): self._get_cci, ('tr',): self._get_tr, ('atr',): self._get_atr, - ('um', 'dm'): self._get_um_dm, ('pdi', 'mdi', 'dx', 'adx', 'adxr'): self._get_dmi, ('trix',): self._get_trix, ('tema',): self._get_tema, diff --git a/test.py b/test.py index f7eeddd..959e129 100644 --- a/test.py +++ b/test.py @@ -522,35 +522,46 @@ def test_get_dma(self): assert_that(c.loc[20160816], near_to(2.15)) assert_that(c.loc[20160815], near_to(2.2743)) + def test_pdm_ndm(self): + c = self.get_stock_90days() + + pdm = c['pdm_14'] + assert_that(pdm.loc[20110104], equal_to(0)) + assert_that(pdm.loc[20110331], near_to(.0842)) + + ndm = c['ndm_14'] + assert_that(ndm.loc[20110104], equal_to(0)) + assert_that(ndm.loc[20110331], near_to(0.0432)) + def test_get_pdi(self): c = self._supor.get('pdi') - assert_that(c.loc[20160817], near_to(24.5989)) - assert_that(c.loc[20160816], near_to(28.6088)) - assert_that(c.loc[20160815], near_to(21.23)) + assert_that(c.loc[20160817], near_to(25.747)) + assert_that(c.loc[20160816], near_to(27.948)) + assert_that(c.loc[20160815], near_to(24.646)) def test_get_mdi(self): c = self._supor.get('mdi') - assert_that(c.loc[20160817], near_to(13.6049)) - assert_that(c.loc[20160816], near_to(15.8227)) - assert_that(c.loc[20160815], near_to(18.8455)) + assert_that(c.loc[20160817], near_to(16.195)) + assert_that(c.loc[20160816], near_to(17.579)) + assert_that(c.loc[20160815], near_to(19.542)) def test_dx(self): c = self._supor.get('dx') - assert_that(c.loc[20160817], near_to(28.7771)) - assert_that(c.loc[20160815], near_to(5.95)) - assert_that(c.loc[20160812], near_to(10.05)) + assert_that(c.loc[20160817], near_to(22.774)) + assert_that(c.loc[20160815], near_to(11.550)) + assert_that(c.loc[20160812], near_to(4.828)) def test_adx(self): c = self._supor.get('adx') - assert_that(c.loc[20160817], near_to(20.1545)) - assert_that(c.loc[20160816], near_to(16.7054)) - assert_that(c.loc[20160815], near_to(11.8767)) + assert_that(c.loc[20160817], near_to(15.535)) + assert_that(c.loc[20160816], near_to(12.640)) + assert_that(c.loc[20160815], near_to(8.586)) def test_adxr(self): c = self._supor.get('adxr') - assert_that(c.loc[20160817], near_to(17.3630)) - assert_that(c.loc[20160816], near_to(16.2464)) - assert_that(c.loc[20160815], near_to(16.0628)) + assert_that(c.loc[20160817], near_to(13.208)) + assert_that(c.loc[20160816], near_to(12.278)) + assert_that(c.loc[20160815], near_to(12.133)) def test_trix_default(self): c = self._supor.get('trix')