From c179ee4c8bf098282db3c8ffc36eac0f7666d629 Mon Sep 17 00:00:00 2001 From: Cedric Zhuang Date: Sun, 18 Jun 2023 00:41:01 +0800 Subject: [PATCH] [GH-145] Add Coppock Curve Coppock Curve is a momentum indicator that signals long-term trend reversals. Formular: Coppock Curve = 10-period WMA of (14-period RoC + 11-period RoC) WMA = Weighted Moving Average RoC = Rate-of-Change Examples: * `df['coppock']` returns the Coppock Curve with default windows * `df['coppock_5,10,15']` returns the Coppock Curve with WMA window 5, fast window 10, slow window 15. --- README.md | 57 ++++++++++ stockstats.py | 289 +++++++++++++++++++++++++++++++++----------------- test.py | 149 +++++++++++++++++--------- 3 files changed, 347 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 350aa0d..0b862b2 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Supported statistics/indicators are: * cross: including upward cross and downward cross * SMA: Simple Moving Average * EMA: Exponential Moving Average +* ROC: Rate of Change * MSTD: Moving Standard Deviation * MVAR: Moving Variance * RSV: Raw Stochastic Value @@ -58,6 +59,9 @@ Supported statistics/indicators are: * Z: Z-Score * AO: Awesome Oscillator * BOP: Balance of Power +* MAD: Mean Absolute Deviation +* ROC: Rate of Change +* Coppock: Coppock Curve ## Installation @@ -364,6 +368,43 @@ It requires column and window. For example, use `df['close_7_smma']` to retrieve the 7 periods smoothed moving average of the close price. +#### [ROC - Rate of Change](https://www.investopedia.com/terms/p/pricerateofchange.asp) + +The Price Rate of Change (ROC) is a momentum-based technical indicator +that measures the percentage change in price between the current price +and the price a certain number of periods ago. + +Formular: + +ROC = (PriceP - PricePn) / PricePn * 100 + +Where: +* PriceP: the price of the current period +* PricePn: the price of the n periods ago + +You need a column name and a period to calculate ROC. + +Examples: +* `df['close_10_roc']`: the ROC of the close price in 10 periods +* `df['high_5_roc']`: the ROC of the high price in 5 periods + +#### [MAD - Mean Absolute Deviation](https://www.khanacademy.org/math/statistics-probability/summarizing-quantitative-data/other-measures-of-spread/a/mean-absolute-deviation-mad-review) + +The mean absolute deviation of a dataset is the average +distance between each data point and the mean. It gives +us an idea about the variability in a dataset. + +Formular: +1. Calculate the mean. +2. Calculate how far away each data point is from the + mean using positive distances. These are called + absolute deviations. +3. Add those deviations together. +4. Divide the sum by the number of data points. + +Example: +* `df['close_10_mad']`: the MAD of the close price in 10 periods + #### [TRIX - Triple Exponential Average](https://www.investopedia.com/articles/technical/02/092402.asp) The triple exponential average is used to identify oversold and overbought @@ -749,6 +790,22 @@ Examples: * `df['cmo']` returns the CMO with a window of 14 * `df['cmo_5']` returns the CMO with a window of 5 +#### [Coppock Curve](https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:coppock_curve) + +Coppock Curve is a momentum indicator that signals +long-term trend reversals. + +Formular: + +Coppock Curve = 10-period WMA of (14-period RoC + 11-period RoC) +WMA = Weighted Moving Average +RoC = Rate-of-Change + +Examples: +* `df['coppock']` returns the Coppock Curve with default windows +* `df['coppock_5,10,15']` returns the Coppock Curve with WMA window 5, + fast window 10, slow window 15. + ## Issues We use [Github Issues](https://github.com/jealous/stockstats/issues) to track diff --git a/stockstats.py b/stockstats.py index e0b9d23..c82161e 100644 --- a/stockstats.py +++ b/stockstats.py @@ -110,6 +110,8 @@ class StockDataFrame(pd.DataFrame): AO_SLOW = 34 AO_FAST = 5 + COPPOCK_PERIODS = (10, 11, 14) + MULTI_SPLIT_INDICATORS = ("kama",) # End of options @@ -245,21 +247,19 @@ def _get_log_ret(self): close = self['close'] self['log-ret'] = np.log(close / self._shift(close, -1)) - def _get_c(self, column, shifts): + def _get_c(self, column, window): """ get the count of column in range (shifts) example: change_20_c :param column: column name - :param shifts: range to count, only to previous + :param window: range to count, only to previous :return: result series """ - column_name = '{}_{}_c'.format(column, shifts) - shifts = self.get_int_positive(shifts) - self[column_name] = self[column].rolling( - center=False, - window=shifts, - min_periods=0).apply(np.count_nonzero) + column_name = '{}_{}_c'.format(column, window) + window = self.get_int_positive(window) + self[column_name] = self._rolling( + self[column], window).apply(np.count_nonzero, raw=True) return self[column_name] def _get_fc(self, column, shifts): @@ -274,10 +274,8 @@ def _get_fc(self, column, shifts): column_name = '{}_{}_fc'.format(column, shifts) shift = self.get_int_positive(shifts) reversed_series = self[column][::-1] - reversed_counts = reversed_series.rolling( - center=False, - window=shift, - min_periods=0).apply(np.count_nonzero) + reversed_counts = self._rolling( + reversed_series, shift).apply(np.count_nonzero, raw=True) counts = reversed_counts[::-1] self[column_name] = counts return counts @@ -312,8 +310,8 @@ def _get_rsv(self, window): """ window = self.get_int_positive(window) column_name = 'rsv_{}'.format(window) - low_min = self._mov_min(self['low'], window) - high_max = self._mov_max(self['high'], window) + low_min = self.mov_min(self['low'], window) + high_max = self.mov_max(self['high'], window) cv = (self['close'] - low_min) / (high_max - low_min) self[column_name] = cv.fillna(0.0) * 100 @@ -361,8 +359,8 @@ def _get_stochrsi(self, window=None): window = self.get_int_positive(window) rsi = self['rsi_{}'.format(window)] - rsi_min = self._mov_min(rsi, window) - rsi_max = self._mov_max(rsi, window) + rsi_min = self.mov_min(rsi, window) + rsi_max = self.mov_max(rsi, window) cv = (rsi - rsi_min) / (rsi_max - rsi_min) self[column_name] = cv * 100 @@ -381,12 +379,12 @@ def _get_wave_trend(self): n2 = self.WAVE_TREND_2 tp = self._tp() - esa = self._ema(tp, n1) - d = self._ema((tp - esa).abs(), n1) + esa = self.ema(tp, n1) + d = self.ema((tp - esa).abs(), n1) ci = (tp - esa) / (0.015 * d) - tci = self._ema(ci, n2) + tci = self.ema(ci, n2) self["wt1"] = tci - self["wt2"] = self._sma(tci, 4) + self["wt2"] = self.sma(tci, 4) @staticmethod def _smma(series, window): @@ -428,9 +426,9 @@ def _get_trix(self, column=None, windows=None): window = self.get_int_positive(windows) - single = self._ema(self[column], window) - double = self._ema(single, window) - triple = self._ema(double, window) + single = self.ema(self[column], window) + double = self.ema(single, window) + triple = self.ema(double, window) prev_triple = self._shift(triple, -1) triple_change = self._delta(triple, -1) self[column_name] = triple_change * 100 / prev_triple @@ -457,9 +455,9 @@ def _get_tema(self, column=None, windows=None): window = self.get_int_positive(windows) - single = self._ema(self[column], window) - double = self._ema(single, window) - triple = self._ema(double, window) + single = self.ema(self[column], window) + double = self.ema(single, window) + triple = self.ema(double, window) self[column_name] = 3 * single - 3 * double + triple def _get_wr(self, window=None): @@ -481,9 +479,9 @@ def _get_wr(self, window=None): column_name = 'wr_{}'.format(window) window = self.get_int_positive(window) - ln = self._mov_min(self['low'], window) + ln = self.mov_min(self['low'], window) - hn = self._mov_max(self['high'], window) + hn = self.mov_max(self['high'], window) self[column_name] = (hn - self['close']) / (hn - ln) * -100 def _get_cci(self, window=None): @@ -507,11 +505,9 @@ def _get_cci(self, window=None): window = self.get_int_positive(window) tp = self._tp() - tp_sma = self._sma(tp, window) - rolling = tp.rolling(min_periods=1, center=False, window=window) - md = rolling.apply(lambda x: np.fabs(x - x.mean()).mean()) - - self[column_name] = (tp - tp_sma) / (.015 * md) + tp_sma = self.sma(tp, window) + mad = self._mad(tp, window) + self[column_name] = (tp - tp_sma) / (.015 * mad) def _tr(self): prev_close = self._shift(self['close'], -1) @@ -633,14 +629,10 @@ def _window_pct(s): n = float(window) return (n - (n - (s + 1))) / n * 100 - high_since = self['high'].rolling( - min_periods=1, - window=window, - center=False).apply(np.argmax) - low_since = self['low'].rolling( - min_periods=1, - window=window, - center=False).apply(np.argmin) + high_since = self._rolling( + self['high'], window).apply(np.argmax, raw=True) + low_since = self._rolling( + self['low'], window).apply(np.argmin, raw=True) aroon_up = _window_pct(high_since) aroon_down = _window_pct(low_since) @@ -667,14 +659,8 @@ def _get_z(self, column, window): window = self.get_int_positive(window) column_name = '{}_{}_z'.format(column, window) col = self[column] - mean = col.rolling( - min_periods=1, - window=window, - center=False).mean() - std = col.rolling( - min_periods=1, - window=window, - center=False).std() + mean = self.sma(col, window) + std = self.mov_std(col, window) self[column_name] = ((col - mean) / std).fillna(0.0) def _atr(self, window): @@ -722,8 +708,8 @@ def _get_dmi(self): self['pdi'] = self._get_pdi(self.PDI_SMMA) self['mdi'] = self._get_mdi(self.MDI_SMMA) self['dx'] = self._get_dx(self.DX_SMMA) - self['adx'] = self._ema(self['dx'], self.ADX_EMA) - self['adxr'] = self._ema(self['adx'], self.ADXR_EMA) + self['adx'] = self.ema(self['dx'], self.ADX_EMA) + self['adxr'] = self.ema(self['adx'], self.ADXR_EMA) def _get_um_dm(self): """ Up move and down move @@ -764,15 +750,15 @@ def _get_vr(self, windows=None): idx = self.index gt_zero = np.where(self['change'] > 0, self['volume'], 0) av = pd.Series(gt_zero, index=idx) - avs = self._mov_sum(av, window) + avs = self.mov_sum(av, window) lt_zero = np.where(self['change'] < 0, self['volume'], 0) bv = pd.Series(lt_zero, index=idx) - bvs = self._mov_sum(bv, window) + bvs = self.mov_sum(bv, window) eq_zero = np.where(self['change'] == 0, self['volume'], 0) cv = pd.Series(eq_zero, index=idx) - cvs = self._mov_sum(cv, window) + cvs = self.mov_sum(cv, window) self[column_name] = (avs + cvs / 2) / (bvs + cvs / 2) * 100 @@ -856,8 +842,8 @@ def _get_cr(self, windows=None): low = self['low'] p1_m = pd.concat((last_middle, high), axis=1).min(axis=1) p2_m = pd.concat((last_middle, low), axis=1).min(axis=1) - p1 = self._mov_sum(high - p1_m, window) - p2 = self._mov_sum(ym - p2_m, window) + p1 = self.mov_sum(high - p1_m, window) + p2 = self.mov_sum(ym - p2_m, window) if windows is None: cr = 'cr' @@ -876,7 +862,7 @@ def _get_cr(self, windows=None): self[cr_ma3] = self._shifted_cr_sma(cr, self.CR_MA3) def _shifted_cr_sma(self, cr, window): - cr_sma = self._sma(cr, window) + cr_sma = self.sma(cr, window) return self._shift(cr_sma, -int(window / 2.5 + 1)) def _tp(self): @@ -944,21 +930,52 @@ def _get_d(self, column, shifts): column_name = '{}_{}_d'.format(column, shift) self[column_name] = self._delta(self[column], shift) - @staticmethod - def _mov_min(series, size): - return series.rolling(min_periods=1, window=size, center=False).min() + @classmethod + def mov_min(cls, series, size): + return cls._rolling(series, size).min() - @staticmethod - def _mov_max(series, size): - return series.rolling(min_periods=1, window=size, center=False).max() + @classmethod + def mov_max(cls, series, size): + return cls._rolling(series, size).max() - @staticmethod - def _mov_sum(series, size): - return series.rolling(min_periods=1, window=size, center=False).sum() + @classmethod + def mov_sum(cls, series, size): + return cls._rolling(series, size).sum() + + @classmethod + def sma(cls, series, size): + return cls._rolling(series, size).mean() @staticmethod - def _sma(series, size): - return series.rolling(min_periods=1, window=size, center=False).mean() + def roc(series, size): + ret = series.diff(size) / series.shift(size) + ret.iloc[:size] = 0 + return ret * 100 + + @classmethod + def _mad(cls, series, window): + """ Mean Absolute Deviation + + :param series: Series + :param window: number of periods + :return: Series + """ + + def f(x): + return np.fabs(x - x.mean()).mean() + + return cls._rolling(series, window).apply(f, raw=True) + + def _get_mad(self, column, window): + """ get mean absolute deviation + + :param column: column to calculate + :param window: number of periods + :return: None + """ + window = self.get_int_positive(window) + column_name = '{}_{}_mad'.format(column, window) + self[column_name] = self._mad(self[column], window) def _get_sma(self, column, windows): """ get simple moving average @@ -969,16 +986,62 @@ def _get_sma(self, column, windows): """ window = self.get_int_positive(windows) column_name = '{}_{}_sma'.format(column, window) - self[column_name] = self._sma(self[column], window) + self[column_name] = self.sma(self[column], window) + + def _get_roc(self, column, window): + """get Rate of Change (ROC) of a column + + The Price Rate of Change (ROC) is a momentum-based technical indicator + that measures the percentage change in price between the current price + and the price a certain number of periods ago. + + https://www.investopedia.com/terms/p/pricerateofchange.asp + + Formular: + + ROC = (PriceP - PricePn) / PricePn * 100 + + Where: + * PriceP: the price of the current period + * PricePn: the price of the n periods ago + + :param column: column to calculate + :param window: window of Rate of Change (ROC) + :return: None + """ + window = self.get_int_positive(window) + column_name = '{}_{}_roc'.format(column, window) + self[column_name] = self.roc(self[column], window) @staticmethod - def _ema(series, window): + def ema(series, window): return series.ewm( ignore_na=False, span=window, min_periods=0, adjust=True).mean() + @staticmethod + def _rolling(series, window): + return series.rolling(window, min_periods=1, center=False) + + @classmethod + def linear_wma(cls, series, window): + total_weight = 0.5 * window * (window + 1) + weights = np.arange(1, window + 1) / total_weight + + def linear(w): + def _compute(x): + try: + return np.dot(x, w) + except ValueError: + return 0.0 + + return _compute + + rolling = cls._rolling(series, window) + return rolling.apply(linear(weights), raw=True) + def _get_ema(self, column, windows): """ get exponential moving average @@ -988,7 +1051,7 @@ def _get_ema(self, column, windows): """ window = self.get_int_positive(windows) column_name = '{}_{}_ema'.format(column, window) - self[column_name] = self._ema(self[column], window) + self[column_name] = self.ema(self[column], window) def _get_boll(self, window=None): """ Get Bollinger bands. @@ -1011,8 +1074,8 @@ def _get_boll(self, window=None): boll = 'boll_{}'.format(n) boll_ub = 'boll_ub_{}'.format(n) boll_lb = 'boll_lb_{}'.format(n) - moving_avg = self._sma(self['close'], n) - moving_std = self._mstd(self['close'], n) + moving_avg = self.sma(self['close'], n) + moving_std = self.mov_std(self['close'], n) self[boll] = moving_avg width = self.BOLL_STD_TIMES * moving_std @@ -1031,10 +1094,10 @@ def _get_macd(self): :return: None """ close = self['close'] - ema_short = self._ema(close, self.MACD_EMA_SHORT) - ema_long = self._ema(close, self.MACD_EMA_LONG) + ema_short = self.ema(close, self.MACD_EMA_SHORT) + ema_long = self.ema(close, self.MACD_EMA_LONG) self['macd'] = ema_short - ema_long - self['macds'] = self._ema(self['macd'], self.MACD_EMA_SIGNAL) + self['macds'] = self.ema(self['macd'], self.MACD_EMA_SIGNAL) self['macdh'] = self['macd'] - self['macds'] def _get_ppo(self): @@ -1052,12 +1115,40 @@ def _get_ppo(self): :return: None """ close = self['close'] - ppo_short = self._ema(close, self.PPO_EMA_SHORT) - ppo_long = self._ema(close, self.PPO_EMA_LONG) + ppo_short = self.ema(close, self.PPO_EMA_SHORT) + ppo_long = self.ema(close, self.PPO_EMA_LONG) self['ppo'] = (ppo_short - ppo_long) / ppo_long * 100 - self['ppos'] = self._ema(self['ppo'], self.PPO_EMA_SIGNAL) + self['ppos'] = self.ema(self['ppo'], self.PPO_EMA_SIGNAL) self['ppoh'] = self['ppo'] - self['ppos'] + def _get_coppock(self, windows=None): + """ Get Coppock Curve + + Coppock Curve is a momentum indicator that signals + long-term trend reversals. + + https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:coppock_curve + + :param windows: collection of window of Coppock Curve + :return: None + """ + if windows is None: + window = self.COPPOCK_PERIODS[0] + fast = self.COPPOCK_PERIODS[1] + slow = self.COPPOCK_PERIODS[2] + column_name = 'coppock' + else: + periods = self.to_ints(windows) + window = periods[0] + fast = periods[1] + slow = periods[2] + column_name = 'coppock_{}'.format(windows) + + fast_roc = self.roc(self['close'], fast) + slow_roc = self.roc(self['close'], slow) + roc_ema = self.linear_wma(fast_roc + slow_roc, window) + self[column_name] = roc_ema + def get_int_positive(self, windows): if isinstance(windows, int): window = windows @@ -1067,9 +1158,9 @@ def get_int_positive(self, windows): raise IndexError("window must be greater than 0") return window - @staticmethod - def _mstd(series, window): - return series.rolling(min_periods=1, window=window, center=False).std() + @classmethod + def mov_std(cls, series, window): + return cls._rolling(series, window).std() def _get_mstd(self, column, windows): """ get moving standard deviation @@ -1080,7 +1171,11 @@ def _get_mstd(self, column, windows): """ window = self.get_int_positive(windows) column_name = '{}_{}_mstd'.format(column, window) - self[column_name] = self._mstd(self[column], window) + self[column_name] = self.mov_std(self[column], window) + + @classmethod + def mov_var(cls, series, window): + return cls._rolling(series, window).var() def _get_mvar(self, column, windows): """ get moving variance @@ -1091,8 +1186,7 @@ def _get_mvar(self, column, windows): """ window = self.get_int_positive(windows) column_name = '{}_{}_mvar'.format(column, window) - self[column_name] = self[column].rolling( - min_periods=1, window=window, center=False).var() + self[column_name] = self.mov_var(self[column], window) def _get_vwma(self, window=None): """ get Volume Weighted Moving Average @@ -1111,8 +1205,8 @@ def _get_vwma(self, window=None): window = self.get_int_positive(window) tpv = self['volume'] * self._tp() - rolling_tpv = self._mov_sum(tpv, window) - rolling_vol = self._mov_sum(self['volume'], window) + rolling_tpv = self.mov_sum(tpv, window) + rolling_vol = self.mov_sum(self['volume'], window) self[column_name] = rolling_tpv / rolling_vol def _get_chop(self, window=None): @@ -1140,9 +1234,9 @@ def _get_chop(self, window=None): column_name = 'chop_{}'.format(window) window = self.get_int_positive(window) atr = self._atr(1) - atr_sum = self._mov_sum(atr, window) - high = self._mov_max(self['high'], window) - low = self._mov_min(self['low'], window) + atr_sum = self.mov_sum(atr, window) + high = self.mov_max(self['high'], window) + low = self.mov_min(self['low'], window) choppy = atr_sum / (high - low) numerator = np.log10(choppy) * 100 denominator = np.log10(window) @@ -1169,8 +1263,8 @@ def _get_mfi(self, window=None): delta = (middle - shifted).fillna(0) pos_flow = money_flow.mask(delta < 0, 0) neg_flow = money_flow.mask(delta >= 0, 0) - rolling_pos_flow = self._mov_sum(pos_flow, window) - rolling_neg_flow = self._mov_sum(neg_flow, window) + rolling_pos_flow = self.mov_sum(pos_flow, window) + rolling_neg_flow = self.mov_sum(neg_flow, window) money_flow_ratio = rolling_pos_flow / (rolling_neg_flow + 1e-12) mfi = (1.0 - 1.0 / (1 + money_flow_ratio)) mfi.iloc[:window] = 0.5 @@ -1202,7 +1296,7 @@ def _get_ao(self, windows=None): column_name = 'ao_{},{}'.format(fast, slow) median_price = (self['high'] + self['low']) * 0.5 - ao = self._sma(median_price, fast) - self._sma(median_price, slow) + ao = self.sma(median_price, fast) - self.sma(median_price, slow) self[column_name] = ao def _get_bop(self): @@ -1240,8 +1334,8 @@ def _get_cmo(self, window=None): close_diff = self['close'].diff() up = close_diff.clip(lower=0) down = close_diff.clip(upper=0).abs() - sum_up = self._mov_sum(up, window) - sum_down = self._mov_sum(down, window) + sum_up = self.mov_sum(up, window) + sum_down = self.mov_sum(down, window) dividend = sum_up - sum_down divisor = sum_up + sum_down res = 100 * dividend / divisor @@ -1272,7 +1366,7 @@ def _get_kama(self, column, windows, fasts=None, slows=None): col_window_s = self._shift(col, -window) col_last = self._shift(col, -1) change = (col - col_window_s).abs() - volatility = self._mov_sum((col - col_last).abs(), window) + volatility = self.mov_sum((col - col_last).abs(), window) efficiency_ratio = change / volatility fast_ema_smoothing = 2.0 / (fast + 1) slow_ema_smoothing = 2.0 / (slow + 1) @@ -1281,7 +1375,7 @@ def _get_kama(self, column, windows, fasts=None, slows=None): smoothing = list(2 * (efficient_smoothing + slow_ema_smoothing)) # start with simple moving average - kama = list(self._sma(col, window)) + kama = list(self.sma(col, window)) col_list = list(col) if len(kama) >= window: last_kama = kama[window - 1] @@ -1432,6 +1526,7 @@ def handler(self): ('ao',): self._get_ao, ('bop',): self._get_bop, ('cmo',): self._get_cmo, + ('coppock',): self._get_coppock, } def __init_not_exist_column(self, key): diff --git a/test.py b/test.py index 75edb80..50ea6c9 100644 --- a/test.py +++ b/test.py @@ -34,7 +34,7 @@ not_, has_item, has_length from numpy import isnan -from stockstats import StockDataFrame as Sdf +from stockstats import StockDataFrame as Sdf, StockDataFrame from stockstats import wrap, unwrap __author__ = 'Cedric Zhuang' @@ -57,13 +57,13 @@ class StockDataFrameTest(TestCase): _stock = wrap(pd.read_csv(get_file('987654.csv'))) _supor = Sdf.retype(pd.read_csv(get_file('002032.csv'))) - def get_stock_20day(self): + def get_stock_20days(self): return self.get_stock().within(20110101, 20110120) - def get_stock_30day(self): + def get_stock_30days(self): return self.get_stock().within(20110101, 20110130) - def get_stock_90day(self): + def get_stock_90days(self): return self.get_stock().within(20110101, 20110331) def get_stock(self): @@ -86,14 +86,14 @@ def test_multiple_columns(self): assert_that(ret.columns, contains_exactly('open', 'close')) def test_column_le_count(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() stock['res'] = stock['close'] <= 13.01 count = stock.get('res_5_c') assert_that(count.loc[20110117], equal_to(1)) assert_that(count.loc[20110119], equal_to(3)) def test_column_ge_future_count(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() stock['res'] = stock['close'] >= 12.8 count = stock['res_5_fc'] assert_that(count.loc[20110119], equal_to(1)) @@ -102,43 +102,43 @@ def test_column_ge_future_count(self): assert_that(count.loc[20110111], equal_to(4)) def test_column_delta(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() open_d = stock['open_-1_d'] assert_that(open_d.loc[20110104], equal_to(0.0)) assert_that(open_d.loc[20110120], near_to(0.07)) def test_column_delta_p2(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() open_d = stock['open_2_d'] assert_that(open_d.loc[20110104], near_to(-0.31)) assert_that(open_d.loc[20110119], equal_to(0.0)) assert_that(open_d.loc[20110118], near_to(-0.2)) def test_column_rate_minus_2(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() open_r = stock['open_-2_r'] assert_that(open_r.loc[20110105], equal_to(0.0)) assert_that(open_r.loc[20110106], near_to(2.495)) def test_column_rate_prev(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() rate = stock['rate'] assert_that(rate.loc[20110107], near_to(4.4198)) def test_column_rate_plus2(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() open_r = stock['open_2_r'] assert_that(open_r.loc[20110118], near_to(-1.566)) assert_that(open_r.loc[20110119], equal_to(0.0)) assert_that(open_r.loc[20110120], equal_to(0.0)) def test_change(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() change = stock['change'] assert_that(change.loc[20110107], near_to(4.4198)) def test_middle(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() middle = stock['middle'] tp = stock['tp'] idx = 20110104 @@ -154,7 +154,7 @@ def test_typical_price_with_amount(self): assert_that(middle[20040817], near_to(11.541)) def test_cr(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() stock.get('cr') assert_that(stock['cr'].loc[20110331], near_to(178.1714)) assert_that(stock['cr-ma1'].loc[20110331], near_to(120.0364)) @@ -168,7 +168,7 @@ def test_cr(self): assert_that(stock['cr_26-ma3'].loc[20110331], near_to(111.5195)) def test_column_permutation(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() amount_p = stock['volume_-1_d_-3,-2,-1_p'] assert_that(amount_p.loc[20110107:20110112], contains_exactly(2, 5, 2, 4)) @@ -177,34 +177,34 @@ def test_column_permutation(self): assert_that(isnan(amount_p.loc[20110106]), equal_to(True)) def test_column_max(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() volume_max = stock['volume_-3,2,-1_max'] assert_that(volume_max.loc[20110106], equal_to(166409700)) assert_that(volume_max.loc[20110120], equal_to(110664100)) assert_that(volume_max.loc[20110112], equal_to(362436800)) def test_column_min(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() volume_max = stock['volume_-3~1_min'] assert_that(volume_max.loc[20110106], equal_to(83140300)) assert_that(volume_max.loc[20110120], equal_to(50888500)) assert_that(volume_max.loc[20110112], equal_to(72035800)) def test_column_shift_positive(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() close_s = stock['close_2_s'] assert_that(close_s.loc[20110118], equal_to(12.48)) assert_that(close_s.loc[20110119], equal_to(12.48)) assert_that(close_s.loc[20110120], equal_to(12.48)) def test_column_shift_zero(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() close_s = stock['close_0_s'] assert_that(close_s.loc[20110118:20110120], contains_exactly(12.69, 12.82, 12.48)) def test_column_shift_negative(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() close_s = stock['close_-2_s'] assert_that(close_s.loc[20110104], equal_to(12.61)) assert_that(close_s.loc[20110105], equal_to(12.61)) @@ -212,66 +212,66 @@ def test_column_shift_negative(self): assert_that(close_s.loc[20110107], equal_to(12.71)) def test_column_rsv(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() rsv_3 = stock['rsv_3'] assert_that(rsv_3.loc[20110106], near_to(60.6557)) def test_column_kdj_default(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() assert_that(stock['kdjk'].loc[20110104], near_to(60.5263)) assert_that(stock['kdjd'].loc[20110104], near_to(53.5087)) assert_that(stock['kdjj'].loc[20110104], near_to(74.5614)) def test_column_kdjk(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() kdjk_3 = stock['kdjk_3'] assert_that(kdjk_3.loc[20110104], near_to(60.5263)) assert_that(kdjk_3.loc[20110120], near_to(31.2133)) def test_column_kdjd(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() kdjk_3 = stock['kdjd_3'] assert_that(kdjk_3.loc[20110104], near_to(53.5087)) assert_that(kdjk_3.loc[20110120], near_to(43.1347)) def test_column_kdjj(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() kdjk_3 = stock['kdjj_3'] assert_that(kdjk_3.loc[20110104], near_to(74.5614)) assert_that(kdjk_3.loc[20110120], near_to(7.37)) def test_column_cross(self): - stock = self.get_stock_30day() + stock = self.get_stock_30days() cross = stock['kdjk_3_x_kdjd_3'] assert_that(sum(cross), equal_to(2)) assert_that(cross.loc[20110114], equal_to(True)) assert_that(cross.loc[20110125], equal_to(True)) def test_column_cross_up(self): - stock = self.get_stock_30day() + stock = self.get_stock_30days() cross = stock['kdjk_3_xu_kdjd_3'] assert_that(sum(cross), equal_to(1)) assert_that(cross.loc[20110125], equal_to(True)) def test_column_cross_down(self): - stock = self.get_stock_30day() + stock = self.get_stock_30days() cross = stock['kdjk_3_xd_kdjd_3'] assert_that(sum(cross), equal_to(1)) assert_that(cross.loc[20110114], equal_to(True)) def test_column_sma(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() sma_2 = stock['open_2_sma'] assert_that(sma_2.loc[20110104], near_to(12.42)) assert_that(sma_2.loc[20110105], near_to(12.56)) def test_column_smma(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() smma = stock['high_5_smma'] assert_that(smma.loc[20110120], near_to(13.0394)) def test_column_ema(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() ema_5 = stock['close_5_ema'] assert_that(ema_5.loc[20110107], near_to(12.9026)) assert_that(ema_5.loc[20110110], near_to(12.9668)) @@ -283,7 +283,7 @@ def test_ema_of_empty_df(): assert_that(len(ema), equal_to(0)) def test_column_macd(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() stock.get('macd') record = stock.loc[20110225] assert_that(record['macd'], near_to(-0.0382)) @@ -291,26 +291,26 @@ def test_column_macd(self): assert_that(record['macdh'], near_to(-0.02805)) def test_column_macds(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() stock.get('macds') record = stock.loc[20110225] assert_that(record['macds'], near_to(-0.0101)) def test_column_macdh(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() stock.get('macdh') record = stock.loc[20110225] assert_that(record['macdh'], near_to(-0.02805)) def test_ppo(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() _ = stock[['ppo', 'ppos', 'ppoh']] assert_that(stock['ppo'].loc[20110331], near_to(1.1190)) assert_that(stock['ppos'].loc[20110331], near_to(0.6840)) assert_that(stock['ppoh'].loc[20110331], near_to(0.4349)) def test_column_mstd(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() mstd_3 = stock['close_3_mstd'] assert_that(mstd_3.loc[20110106], near_to(0.05033)) @@ -339,12 +339,12 @@ def test_bollinger_empty(self): assert_that(len(s), equal_to(0)) def test_column_mvar(self): - stock = self.get_stock_20day() + stock = self.get_stock_20days() mvar_3 = stock['open_3_mvar'] assert_that(mvar_3.loc[20110106], near_to(0.0292)) def test_column_parse_error(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() with self.assertRaises(UserWarning): _ = stock["foobarbaz"] with self.assertRaises(KeyError): @@ -445,7 +445,7 @@ def test_parse_cross_column_xu(): contains_exactly('a', 'xu', 'b')) def test_get_log_ret(self): - stock = self.get_stock_30day() + stock = self.get_stock_30days() stock.get('log-ret') assert_that(stock.loc[20110128]['log-ret'], near_to(-0.010972)) @@ -474,7 +474,7 @@ def test_get_rsi(self): assert_that(rsi.loc[idx], near_to(rsi_14.loc[idx])) def test_get_stoch_rsi(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() stoch_rsi = stock['stochrsi'] stoch_rsi_6 = stock['stochrsi_6'] stoch_rsi_14 = stock['stochrsi_14'] @@ -590,7 +590,7 @@ def test_vr_ma(self): assert_that(c.loc[20160815], near_to(197.5225)) def test_mfi(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() first = 20110104 last = 20110331 @@ -607,7 +607,7 @@ def test_mfi(self): assert_that(mfi_15.loc[last], near_to(0.6733)) def test_column_kama(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() idx = 20110331 kama_10 = stock['close_10_kama_2_30'] assert_that(kama_10.loc[idx], near_to(13.6648)) @@ -615,7 +615,7 @@ def test_column_kama(self): assert_that(kama_2.loc[idx], near_to(13.7326)) def test_vwma(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() vwma = stock['vwma'] vwma_7 = stock['vwma_7'] vwma_14 = stock['vwma_14'] @@ -626,7 +626,7 @@ def test_vwma(self): assert_that(vwma_7.loc[idx], is_not(near_to(vwma.loc[idx]))) def test_chop(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() chop = stock['chop'] chop_7 = stock['chop_7'] chop_14 = stock['chop_14'] @@ -636,27 +636,27 @@ def test_chop(self): assert_that(chop_7.loc[idx], is_not(near_to(chop.loc[idx]))) def test_column_conflict(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() res = stock[['close_26_ema', 'macd']] idx = 20110331 assert_that(res['close_26_ema'].loc[idx], near_to(13.2488)) assert_that(res['macd'].loc[idx], near_to(0.1482)) def test_wave_trend(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() wt1, wt2 = stock['wt1'], stock['wt2'] idx = 20110331 assert_that(wt1.loc[idx], near_to(38.9610)) assert_that(wt2.loc[idx], near_to(31.6997)) def test_init_all(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() stock.init_all() columns = stock.columns assert_that(columns, has_items('macd', 'kdjj', 'mfi', 'boll')) def test_supertrend(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() st = stock['supertrend'] st_ub = stock['supertrend_ub'] st_lb = stock['supertrend_lb'] @@ -672,7 +672,7 @@ def test_supertrend(self): assert_that(st_lb[idx], near_to(12.9021)) def test_ao(self): - stock = self.get_stock_90day() + stock = self.get_stock_90days() ao = stock['ao'] ao1 = stock['ao_5,34'] ao2 = stock['ao_5,10'] @@ -682,13 +682,13 @@ def test_ao(self): assert_that(ao2[idx], near_to(-0.071)) def test_bop(self): - stock = self.get_stock_30day() + stock = self.get_stock_30days() bop = stock['bop'] assert_that(bop[20110104], near_to(0.5)) assert_that(bop[20110106], near_to(-0.207)) def test_cmo(self): - stock = self.get_stock_30day() + stock = self.get_stock_30days() cmo = stock['cmo'] assert_that(cmo[20110104], equal_to(0)) assert_that(cmo[20110126], near_to(7.023)) @@ -774,3 +774,50 @@ def test_close_z(self): assert_that(stock.loc[20040817, 'close_14_z'], equal_to(0)) assert_that(stock.loc[20040915, 'close_14_z'], near_to(2.005)) assert_that(stock.loc[20041014, 'close_14_z'], near_to(-2.014)) + + def test_roc(self): + stock = self._supor[:100] + _ = stock['high_5_roc'] + assert_that(stock.loc[20040817, 'high_5_roc'], equal_to(0)) + assert_that(stock.loc[20040915, 'high_5_roc'], near_to(5.912)) + assert_that(stock.loc[20041014, 'high_5_roc'], near_to(5.009)) + assert_that(stock.loc[20041220, 'high_5_roc'], near_to(-4.776)) + + s = StockDataFrame.roc(stock['high'], size=5) + assert_that(s.loc[20040915], near_to(5.912)) + + def test_mad(self): + stock = self.get_stock_30days() + s = stock['close_5_mad'] + assert_that(s[20110104], equal_to(0)) + assert_that(s[20110114], near_to(0.146)) + + @staticmethod + def test_mad_raw(): + series = pd.Series([10, 15, 15, 17, 18, 21]) + res = StockDataFrame._mad(series, 6) + assert_that(res[5], near_to(2.667)) + + @staticmethod + def test_linear_wma(): + series = pd.Series([10, 15, 15, 17, 18, 21]) + res = StockDataFrame.linear_wma(series, 6) + assert_that(res[0], equal_to(0)) + assert_that(res[5], near_to(17.571)) + + def test_coppock(self): + stock = self.get_stock_90days() + c0 = stock['coppock'] + assert_that(c0[20110117], equal_to(0)) + assert_that(c0[20110221], near_to(3.293)) + assert_that(c0[20110324], near_to(-2.274)) + + c1 = stock['coppock_10,11,14'] + assert_that(c1[20110117], equal_to(0)) + assert_that(c1[20110221], near_to(3.293)) + assert_that(c1[20110324], near_to(-2.274)) + + c2 = stock['coppock_5,10,15'] + assert_that(c2[20110117], equal_to(0)) + assert_that(c2[20110221], near_to(4.649)) + assert_that(c2[20110324], near_to(-2.177))