From 10f5cc2cbb7e3d7085e7fc57ac11fe0d3cdc09b8 Mon Sep 17 00:00:00 2001 From: Ana Ruelas Date: Mon, 14 Nov 2016 16:35:56 -0500 Subject: [PATCH] ENH: Actually use rolling windows for EWMA in MACD --- tests/pipeline/test_technical.py | 77 +++++++++++++++++++++ zipline/pipeline/factors/__init__.py | 6 ++ zipline/pipeline/factors/technical.py | 97 ++++++++++++++++----------- 3 files changed, 141 insertions(+), 39 deletions(-) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index 7ef0e032..e49ebcf8 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -5,6 +5,7 @@ from six.moves import range import numpy as np import pandas as pd import talib +from numpy.random import random_integers from zipline.lib.adjusted_array import AdjustedArray from zipline.pipeline.data import USEquityPricing @@ -16,6 +17,7 @@ from zipline.pipeline.factors import ( LinearWeightedMovingAverage, RateOfChangePercentage, TrueRange, + MovingAverageConvergenceDivergence ) from zipline.testing import parameter_space from zipline.testing.fixtures import ZiplineTestCase @@ -403,3 +405,78 @@ class TestTrueRange(ZiplineTestCase): tr.compute(today, assets, out, highs, lows, closes) assert_equal(out, np.full((3,), 2.)) + + +class MovingAverageConvergenceDivergenceCase(ZiplineTestCase): + def test_MACD_window_length_generation(self): + signal_period = random_integers(1, 90) + fast_period = random_integers(signal_period+1, signal_period+100) + slow_period = random_integers(fast_period+1, fast_period+100) + ewma = MovingAverageConvergenceDivergence( + fast_period=fast_period, + slow_period=slow_period, + signal_period=signal_period, + ) + assert_equal( + ewma.window_length, + slow_period+signal_period-1, + ) + + def test_moving_average_convergence_divergence(self): + fast_period = 3 + slow_period = 8 + signal_period = 2 + + macd = MovingAverageConvergenceDivergence( + fast_period=fast_period, + slow_period=slow_period, + signal_period=signal_period, + ) + + today = pd.Timestamp('2016', tz='utc') + nassets = macd.window_length + assets = pd.Index(np.arange(nassets)) + days_col = np.arange(start=-.05, + stop=.01*nassets-.05, + step=.01)[:, np.newaxis] + close = np.logspace(start=.01, stop=.10, num=nassets) - 1 + days_col + + dtype = [ + ('macd', 'f8'), + ('signal', 'f8'), + ('hist', 'f8'), + ] + out = np.recarray( + shape=(nassets,), + dtype=dtype, + buf=np.empty(shape=(nassets,), dtype=dtype), + ) + macd.compute( + today, + assets, + out, + close, + fast_period, + slow_period, + signal_period, + ) + + expected_macd = np.array([0.01691553] * nassets) + expected_signal = np.array([0.01691553] * nassets) + expected_hist = np.array([0] * nassets) + + np.testing.assert_almost_equal( + out.macd, + expected_macd, + decimal=8 + ) + np.testing.assert_almost_equal( + out.signal, + expected_signal, + decimal=8 + ) + np.testing.assert_almost_equal( + out.hist, + expected_hist, + decimal=8 + ) diff --git a/zipline/pipeline/factors/__init__.py b/zipline/pipeline/factors/__init__.py index 0155cab6..73e8f5fe 100644 --- a/zipline/pipeline/factors/__init__.py +++ b/zipline/pipeline/factors/__init__.py @@ -32,6 +32,9 @@ from .technical import ( TrueRange, VWAP, WeightedAverageValue, + MovingAverageConvergenceDivergence, + MACD, + AnnualizedVolatility, ) __all__ = [ @@ -62,4 +65,7 @@ __all__ = [ 'TrueRange', 'VWAP', 'WeightedAverageValue', + 'MovingAverageConvergenceDivergence', + 'MACD', + 'AnnualizedVolatility', ] diff --git a/zipline/pipeline/factors/technical.py b/zipline/pipeline/factors/technical.py index bdbc49ae..4cf2f328 100644 --- a/zipline/pipeline/factors/technical.py +++ b/zipline/pipeline/factors/technical.py @@ -21,7 +21,6 @@ from numpy import ( NINF, sqrt, sum as np_sum, - nan ) from numexpr import evaluate @@ -38,10 +37,9 @@ from zipline.utils.math_utils import ( nansum, nanmin, ) +from zipline.utils.numpy_utils import rolling_window from .factor import CustomFactor -from talib import MACD - class Returns(CustomFactor): """ @@ -448,11 +446,6 @@ class ExponentialWeightedMovingStdDev(_ExponentialWeightedFactor): out[:] = sqrt(variance * bias_correction) -# Convenience aliases. -EWMA = ExponentialWeightedMovingAverage -EWMSTD = ExponentialWeightedMovingStdDev - - class BollingerBands(CustomFactor): """ Bollinger Bands technical indicator. @@ -688,8 +681,7 @@ class TrueRange(CustomFactor): ) - -class MovingAverageConvergenceDivergence(CustomFactor): +class MovingAverageConvergenceDivergence(_ExponentialWeightedFactor): """ Moving Average Convergence/Divergence (MACD) https://en.wikipedia.org/wiki/MACD @@ -700,21 +692,22 @@ class MovingAverageConvergenceDivergence(CustomFactor): trend in a stock's price. **Default Inputs:** :data:`zipline.pipeline.data.USEquityPricing.close` - **Default Window Length:** None + **Default Window Length:** Window length is automatically calculated as the + sum of slow_period and signal_period. Parameters ---------- fast_period : int >= 0, <= window_length - The window length for the "fast" EMA. + The window length for the "fast" EWMA. Default is 12. slow_period : int >= 0, <= window_length - The window length for the "slow" EMA. + The window length for the "slow" EWMA. Default is 26. signal_period' : int >= 0, <= slow_period - The window length for the signal line. + The window length for the signal line. Default is 9. Returns ------- - MACD: The difference between "fast" EMA and "slow" EMA. - signal: The signal_period length period EMA of the MACD line. + MACD: The difference between "fast" EWMA and "slow" EWMA. + signal: The EWMA of the MACD line using `signal_period` as span. hist: Difference between MACD and signal. (Divergence series) """ inputs = [USEquityPricing.close] @@ -728,47 +721,73 @@ class MovingAverageConvergenceDivergence(CustomFactor): *args, **kwargs): return super(MovingAverageConvergenceDivergence, cls).__new__( + cls, fast_period=fast_period, slow_period=slow_period, signal_period=signal_period, - window_length=slow_period + signal_period, + window_length=slow_period + signal_period - 1, *args, **kwargs ) - def calculate_macd(self, col, fast, slow, signal): - try: - macd, sig, hist = MACD(col, - fastperiod=fast, - slowperiod=slow, - signalperiod=signal) - return macd[-1], sig[-1], hist[-1] - except: - return nan, nan, nan + def calculate_ewma(self, data, length): + decay_rate = 1.0 - (2.0 / (1.0 + length)) + return average(data, + axis=1, + weights=self.weights(length, decay_rate)) + + def calculate_macd(self, col): + slow_EWMA = self.calculate_ewma( + rolling_window( + col, + self.params['slow_period'] + ), + self.params['slow_period']) + fast_EWMA = self.calculate_ewma( + rolling_window( + col, + self.params['fast_period'] + )[-self.params['signal_period']:], + self.params['fast_period']) + macd = fast_EWMA - slow_EWMA + signal_line = self.calculate_ewma( + macd.reshape(-1, self.params['signal_period']), + self.params['signal_period']) + hist = macd[-1] - signal_line + return macd[-1], signal_line[-1], hist[-1] def compute(self, today, assets, out, close, fast_period, slow_period, signal_period): - n = len(close) - macd, sig, hist = zip(*map(self.calculate_macd, - close.T, - [fast_period]*n, - [slow_period]*n, - [signal_period]*n)) - out.MACD[:] = macd + macd, sig, hist = zip(*map(self.calculate_macd, close.T)) + out.macd[:] = macd out.signal[:] = sig out.hist[:] = hist -class AnnualVolatility(CustomFactor): +class AnnualizedVolatility(CustomFactor): """ Volatility https://en.wikipedia.org/wiki/Volatility_(finance) The degree of variation of a series over time as measured by the standard - deviation of the data over the course of a year. + deviation of returns. - **Default Inputs:** :data:`zipline.pipeline.data.USEquityPricing.close` + **Default Inputs:** + :data:`zipline.pipeline.factors.Returns(window_length=2)` + + Parameters + ---------- + annualization_factor : + The number of time units per year. Defaults to average number of NYSE + trading days per year, 252. """ - inputs = [USEquityPricing.close] + inputs = [Returns(window_length=2)] + params = {'annualization_factor': 252} + window_length = 252 - def compute(self, today, assets, out, closes): - out[:] = nanstd(closes, ddof=1, axis=0) * (252 ** 0.5) + def compute(self, today, assets, out, returns, annualization_factor): + out[:] = nanstd(returns, ddof=0, axis=0) * (annualization_factor ** .5) + +# Convenience aliases. +EWMA = ExponentialWeightedMovingAverage +EWMSTD = ExponentialWeightedMovingStdDev +MACD = MovingAverageConvergenceDivergence