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')