diff --git a/README.md b/README.md index 9c968c9..2176400 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Supported statistics/indicators are: * Inertia: Inertia Indicator * KST: Know Sure Thing * PGO: Pretty Good Oscillator +* PSL: Psychological Line ## Installation @@ -1008,6 +1009,19 @@ Example: * `df['pgo']` retrieves the PGO with default window 14. * `df['pgo_10']` retrieves the PGO with window 10. +#### [Psychological Line (PSL)](https://library.tradingtechnologies.com/trade/chrt-ti-psychological-line.html) + +The Psychological Line indicator is the ratio of the number of +rising periods over the total number of periods. + +Formular: +* PSL = (Number of Rising Periods) / (Total Number of Periods) * 100 + +Example: +* `df['psl']` retrieves the PSL with default window 12. +* `df['psl_10']` retrieves the PSL with window 10. +* `df['high_12_psl']` retrieves the PSL of high price with window 10. + ## Issues diff --git a/stockstats.py b/stockstats.py index 267dd9f..7e5be16 100644 --- a/stockstats.py +++ b/stockstats.py @@ -73,6 +73,7 @@ class StockStatsError(Exception): 'pdi': 14, 'pgo': 14, 'ppo': (12, 26, 9), # short, long, signal + 'psl': 12, 'rsi': 14, 'rsv': 9, 'rvgi': 14, @@ -99,6 +100,7 @@ def set_dft_window(name: str, windows: Union[int, tuple[int, ...]]): 'dma': 'close', 'kama': 'close', 'ker': 'close', + 'psl': 'close', 'tema': 'close', 'trix': 'close', } @@ -1605,6 +1607,29 @@ def _pgo(self, window: int) -> pd.Series: def _get_pgo(self, meta: _Meta): self[meta.name] = self._pgo(meta.int) + def _psl(self, col_name: str, window: int) -> pd.Series: + """ Psychological Line (PSL) + + The Psychological Line indicator is the ratio of the number of + rising periods over the total number of periods. + + https://library.tradingtechnologies.com/trade/chrt-ti-psychological-line.html + + Formular: + * PSL = (Number of Rising Periods) / (Total Number of Periods) * 100 + + Example: + * `df['psl']` retrieves the PSL with default window 12. + * `df['psl_10']` retrieves the PSL with window 10. + * `df['high_12_psl']` retrieves the PSL of high price with window 10. + """ + col_diff = self._col_diff(col_name) + pos = col_diff > 0 + return self.mov_sum(pos, window) / window * 100 + + def _get_psl(self, meta: _Meta): + self[meta.name] = self._psl(meta.column, meta.int) + @staticmethod def parse_column_name(name): m = re.match(r'(.*)_([\d\-+~,.]+)_(\w+)', name) @@ -1702,57 +1727,36 @@ def drop_head(self, n, inplace=False): return self return wrap(ret) + def _get_handler(self, name: str): + return getattr(self, f'_get_{name}') + @property def handler(self): - return { - ('change',): self._get_change, - ('rsi',): self._get_rsi, - ('stochrsi',): self._get_stochrsi, + ret = { ('rate',): self._get_rate, ('middle',): self._get_middle, ('tp',): self._get_tp, ('boll', 'boll_ub', 'boll_lb'): self._get_boll, ('macd', 'macds', 'macdh'): self._get_macd, ('ppo', 'ppos', 'ppoh'): self._get_ppo, - ('kdjk',): self._get_kdjk, - ('kdjd',): self._get_kdjd, - ('kdjj',): self._get_kdjj, - ('rsv',): self._get_rsv, ('cr', 'cr-ma1', 'cr-ma2', 'cr-ma3'): self._get_cr, - ('cci',): self._get_cci, ('tr',): self._get_tr, - ('atr',): self._get_atr, - ('pdi',): self._get_pdi, - ('ndi',): self._get_ndi, ('dx', 'adx', 'adxr'): self._get_dmi, - ('trix',): self._get_trix, - ('tema',): self._get_tema, - ('vr',): self._get_vr, - ('dma',): self._get_dma, - ('vwma',): self._get_vwma, - ('chop',): self._get_chop, ('log-ret',): self._get_log_ret, - ('mfi',): self._get_mfi, ('wt1', 'wt2'): self._get_wt, - ('wr',): self._get_wr, ('supertrend', 'supertrend_lb', 'supertrend_ub'): self._get_supertrend, - ('aroon',): self._get_aroon, - ('ao',): self._get_ao, ('bop',): self._get_bop, - ('cmo',): self._get_cmo, - ('coppock',): self._get_coppock, - ('ichimoku',): self._get_ichimoku, ('cti',): self._get_cti, - ('ker',): self._get_ker, ('eribull', 'eribear'): self._get_eri, - ('ftr',): self._get_ftr, ('rvgi', 'rvgis'): self._get_rvgi, - ('inertia',): self._get_inertia, ('kst',): self._get_kst, - ('pgo',): self._get_pgo, } + for k in _dft_windows.keys(): + if k not in ret: + ret[k] = self._get_handler(k) + return ret def __init_not_exist_column(self, key): for names, handler in self.handler.items(): @@ -1774,7 +1778,7 @@ def __init_not_exist_column(self, key): raise UserWarning("Invalid number of return arguments " f"after parsing column name: '{key}'") meta = _Meta(name, windows=n, column=col) - getattr(self, f'_get_{name}')(meta) + self._get_handler(name)(meta) def __init_column(self, key): if key not in self: diff --git a/test.py b/test.py index 8b58da0..7337384 100644 --- a/test.py +++ b/test.py @@ -744,7 +744,10 @@ def test_init_all(self): stock = self.get_stock_90days() stock.init_all() columns = stock.columns - assert_that(columns, has_items('macd', 'kdjj', 'mfi', 'boll')) + assert_that(columns, has_items( + 'macd', 'kdjj', 'mfi', 'boll', + 'adx', 'cr-ma2', 'supertrend_lb', 'boll_lb', + 'ao', 'cti', 'ftr', 'psl')) def test_supertrend(self): stock = self.get_stock_90days() @@ -1058,3 +1061,21 @@ def test_pgo(self): pgo10 = stock['pgo_10'] assert_that(pgo10[20110117], near_to(-0.959768)) assert_that(pgo10[20110214], near_to(1.214206)) + + def test_psl(self): + stock = self.get_stock_90days() + psl = stock['psl'] + assert_that(psl[20110118], near_to(41.666)) + assert_that(psl[20110127], near_to(50)) + + psl12 = stock['psl_12'] + assert_that(psl12[20110118], near_to(41.666)) + assert_that(psl12[20110127], near_to(50)) + + psl10 = stock['psl_10'] + assert_that(psl10[20110118], near_to(50)) + assert_that(psl10[20110131], near_to(60)) + + high_psl12 = stock['high_12_psl'] + assert_that(high_psl12[20110118], near_to(41.666)) + assert_that(high_psl12[20110127], near_to(41.666))