diff --git a/README.md b/README.md index 456b420..c7b0491 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Supported statistics/indicators are: * LRMA: Linear Regression Moving Average * ERI: Elder-Ray Index * FTR: the Gaussian Fisher Transform Price Reversals indicator +* RVGI: Relative Vigor Index ## Installation @@ -923,6 +924,42 @@ Examples: * `df['ftr']` returns the FTR with window 9 * `df['ftr_20']` returns the FTR with window 20 +#### [Relative Vigor Index (RVGI)](https://www.investopedia.com/terms/r/relative_vigor_index.asp) + +The Relative Vigor Index (RVI) is a momentum indicator +used in technical analysis that measures the strength +of a trend by comparing a security's closing price to +its trading range while smoothing the results using a +simple moving average (SMA). + +Formular + +* NUMERATOR= (a+(2×b)+(2×c)+d) / 6 +* DENOMINATOR= (e+(2×f)+(2×g)+h) / 6 +* RVI= SMA-N of DENOMINATOR / SMA-N of NUMERATOR +* Signal Line = (RVI+(2×i)+(2×j)+k) / 6 + +where: + +* a=Close−Open +* b=Close−Open One Bar Prior to a +* c=Close−Open One Bar Prior to b +* d=Close−Open One Bar Prior to c +* e=High−Low of Bar a +* f=High−Low of Bar b +* g=High−Low of Bar c +* h=High−Low of Bar d +* i=RVI Value One Bar Prior +* j=RVI Value One Bar Prior to i +* k=RVI Value One Bar Prior to j +* N=Minutes/Hours/Days/Weeks/Months + +Examples: +* `df['rvgi']` retrieves the RVGI line of window 14 +* `df['rvgis']` retrieves the RVGI signal line of window 14 +* `df['rvgi_5']` retrieves the RVGI line of window 5 +* `df['rvgis_5']` retrieves the RVGI signal line of window 5 + ## Issues We use [Github Issues](https://github.com/jealous/stockstats/issues) to track diff --git a/stockstats.py b/stockstats.py index f5bc21b..9e6e0bf 100644 --- a/stockstats.py +++ b/stockstats.py @@ -73,6 +73,7 @@ class StockStatsError(Exception): 'ppo': (12, 26, 9), # short, long, signal 'rsi': 14, 'rsv': 9, + 'rvgi': 14, 'stochrsi': 14, 'supertrend': 14, 'tema': 5, @@ -198,6 +199,10 @@ def name(self): return f'{self._name}_{self._windows}' return f'{self.column}_{self.windows}_{self._name}' + def set_name(self, name: str): + self._name = name + return self + def name_ex(self, ex): ret = f'{self._name}{ex}' if self._windows is None: @@ -1429,7 +1434,7 @@ def _get_kama(self, meta: _Meta): last_kama = cur self[meta.name] = kama - def _ftr(self, window: int): + def _ftr(self, window: int) -> pd.Series: mp = (self.high + self.low) * 0.5 highest = mp.rolling(window).max() lowest = mp.rolling(window).min() @@ -1470,6 +1475,65 @@ def _get_ftr(self, meta: _Meta): """ self[meta.name] = self._ftr(meta.int) + @staticmethod + def sym_wma4(series: pd.Series) -> pd.Series: + arr = np.array([1, 2, 2, 1]) + weights = arr / sum(arr) + rolled = series.rolling(arr.size) + ret = rolled.apply(lambda x: np.dot(x, weights), raw=True) + ret.iloc[:arr.size - 1] = 0.0 + return ret + + def _rvgi(self, window: int) -> pd.Series: + """ Relative Vigor Index (RVGI) + + The Relative Vigor Index (RVI) is a momentum indicator + used in technical analysis that measures the strength + of a trend by comparing a security's closing price to + its trading range while smoothing the results using a + simple moving average (SMA). + + https://www.investopedia.com/terms/r/relative_vigor_index.asp + + Formular + * NUMERATOR= (a+(2×b)+(2×c)+d) / 6 + * DENOMINATOR= (e+(2×f)+(2×g)+h) / 6 + * RVI= SMA-N of DENOMINATOR / SMA-N of NUMERATOR + * Signal Line = (RVI+(2×i)+(2×j)+k) / 6 + + where: + * a=Close−Open + * b=Close−Open One Bar Prior to a + * c=Close−Open One Bar Prior to b + * d=Close−Open One Bar Prior to c + * e=High−Low of Bar a + * f=High−Low of Bar b + * g=High−Low of Bar c + * h=High−Low of Bar d + * i=RVI Value One Bar Prior + * j=RVI Value One Bar Prior to i + * k=RVI Value One Bar Prior to j + * N=Minutes/Hours/Days/Weeks/Months + """ + co = self.close - self.open + hl = self.high - self.low + + nu = self.sym_wma4(co) + de = self.sym_wma4(hl) + ret = self.sma(nu, window) / self.sma(de, window) + return ret + + def _get_rvgis(self, meta: _Meta): + self._get_rvgi(meta.set_name('rvgi')) + + def _get_rvgi(self, meta: _Meta): + rvgi = self._rvgi(meta.int) + rvgi.iloc[:3] = 0.0 + rvgi_s = self.sym_wma4(rvgi) + rvgi_s.iloc[:6] = 0.0 + self[meta.name] = rvgi + self[meta.name_ex('s')] = rvgi_s + @staticmethod def parse_column_name(name): m = re.match(r'(.*)_([\d\-+~,.]+)_(\w+)', name) @@ -1613,6 +1677,7 @@ def handler(self): ('ker',): self._get_ker, ('eribull', 'eribear'): self._get_eri, ('ftr',): self._get_ftr, + ('rvgi', 'rvgis'): self._get_rvgi, } def __init_not_exist_column(self, key): diff --git a/test.py b/test.py index d9f822e..d9e1fba 100644 --- a/test.py +++ b/test.py @@ -889,6 +889,15 @@ def test_mad_raw(): res = StockDataFrame._mad(series, 6) assert_that(res[5], near_to(2.667)) + @staticmethod + def test_sym_wma4(): + series = pd.Series([4, 2, 2, 4, 8]) + res = StockDataFrame.sym_wma4(series) + assert_that(res[0], equal_to(0)) + assert_that(res[2], equal_to(0)) + assert_that(res[3], near_to(2.666)) + assert_that(res[4], near_to(3.666)) + def test_ichimoku(self): stock = self.get_stock_90days() i0 = stock['ichimoku'] @@ -977,6 +986,21 @@ def test_ftr(self): f = stock['ftr_15'] assert_that(f[20110128], near_to(-1.005)) + def test_rvgi(self): + stock = self.get_stock_30days() + r, s = stock['rvgi'], stock['rvgis'] + r14, s14 = stock['rvgi_14'], stock['rvgis_14'] + assert_that(r[20110128], equal_to(r14[20110128])) + assert_that(s[20110128], equal_to(s14[20110128])) + assert_that(r[20110106], equal_to(0)) + assert_that(r[20110107], near_to(0.257)) + assert_that(s[20110111], equal_to(0)) + assert_that(s[20110112], near_to(0.303)) + + s10, r10 = stock['rvgis_10'], stock['rvgi_10'] + assert_that(r10[20110128], near_to(-0.056)) + assert_that(s10[20110128], near_to(-0.043)) + def test_change_group_window_defaults(self): stock = self.get_stock_90days() macd = stock['macd']