Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-150] Fix pdm and ndm #151

Merged
merged 1 commit into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
100 changes: 58 additions & 42 deletions stockstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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]

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 26 additions & 15 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down