diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index cd5ada22..78976893 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -438,6 +438,35 @@ class MovingAverageConvergenceDivergenceTestCase(ZiplineTestCase): slow_period + signal_period - 1, ) + def test_bad_inputs(self): + template = ( + "MACDSignal() expected a value greater than or equal to 1" + " for argument %r, but got 0 instead." + ) + with self.assertRaises(ValueError) as e: + MovingAverageConvergenceDivergenceSignal(fast_period=0) + self.assertEqual(template % 'fast_period', str(e.exception)) + + with self.assertRaises(ValueError) as e: + MovingAverageConvergenceDivergenceSignal(slow_period=0) + self.assertEqual(template % 'slow_period', str(e.exception)) + + with self.assertRaises(ValueError) as e: + MovingAverageConvergenceDivergenceSignal(signal_period=0) + self.assertEqual(template % 'signal_period', str(e.exception)) + + with self.assertRaises(ValueError) as e: + MovingAverageConvergenceDivergenceSignal( + fast_period=5, + slow_period=4, + ) + + expected = ( + "'slow_period' must be greater than 'fast_period', but got\n" + "slow_period=4, fast_period=5" + ) + self.assertEqual(expected, str(e.exception)) + @parameter_space( seed=range(2), fast_period=[3, 5], @@ -478,14 +507,23 @@ class MovingAverageConvergenceDivergenceTestCase(ZiplineTestCase): close_df = pd.DataFrame(close) fast_ewma = self.expected_ewma( close_df, - fast_period) + fast_period, + ) slow_ewma = self.expected_ewma( close_df, - slow_period) - expected_signal = self.expected_ewma( - fast_ewma-slow_ewma, + slow_period, + ) + signal_ewma = self.expected_ewma( + fast_ewma - slow_ewma, signal_period - ).values[-1] + ) + + # Everything but the last row should be NaN. + self.assertTrue(signal_ewma.iloc[:-1].isnull().all().all()) + + # We're testing a single compute call, which we expect to be equivalent + # to the last row of the frame we calculated with pandas. + expected_signal = signal_ewma.values[-1] np.testing.assert_almost_equal( out, @@ -505,7 +543,7 @@ class AnnualizedVolatilityTestCase(ZiplineTestCase): nassets = 3 ann_vol = AnnualizedVolatility() today = pd.Timestamp('2016', tz='utc') - assets = np.arange(nassets, dtype=np.float) + assets = np.arange(nassets, dtype=np.float64) returns = np.full((ann_vol.window_length, nassets), 0.004, dtype=np.float64) @@ -527,7 +565,7 @@ class AnnualizedVolatilityTestCase(ZiplineTestCase): nassets = 3 ann_vol = AnnualizedVolatility() today = pd.Timestamp('2016', tz='utc') - assets = np.arange(nassets, dtype=np.float) + assets = np.arange(nassets, dtype=np.float64) returns = np.random.normal(loc=0.001, scale=0.01, size=(ann_vol.window_length, nassets)) diff --git a/zipline/pipeline/factors/technical.py b/zipline/pipeline/factors/technical.py index 947b7495..c7363bba 100644 --- a/zipline/pipeline/factors/technical.py +++ b/zipline/pipeline/factors/technical.py @@ -26,8 +26,7 @@ from numexpr import evaluate from zipline.pipeline.data import USEquityPricing from zipline.pipeline.mixins import SingleInputMixin -from zipline.utils.numpy_utils import ignore_nanwarnings -from zipline.utils.input_validation import expect_types +from zipline.utils.input_validation import expect_bounded, expect_types from zipline.utils.math_utils import ( nanargmax, nanargmin, @@ -37,7 +36,11 @@ from zipline.utils.math_utils import ( nansum, nanmin, ) -from zipline.utils.numpy_utils import rolling_window +from zipline.utils.numpy_utils import ( + float64_dtype, + ignore_nanwarnings, + rolling_window, +) from .factor import CustomFactor @@ -401,13 +404,13 @@ class LinearWeightedMovingAverage(CustomFactor, SingleInputMixin): ctx = ignore_nanwarnings() def compute(self, today, assets, out, data): - num_days = data.shape[0] + ndays = data.shape[0] # Initialize weights array - weights = arange(1, num_days + 1, dtype=float).reshape(num_days, 1) + weights = arange(1, ndays + 1, dtype=float64_dtype).reshape(ndays, 1) # Compute normalizer - normalizer = (num_days * (num_days + 1)) / 2 + normalizer = (ndays * (ndays + 1)) / 2 # Weight the data weighted_data = data * weights @@ -706,8 +709,6 @@ class MovingAverageConvergenceDivergenceSignal(CustomFactor): trend in a stock's price. **Default Inputs:** :data:`zipline.pipeline.data.USEquityPricing.close` - **Default Window Length:** Window length is automatically calculated as the - sum of slow_period and signal_period. Parameters ---------- @@ -718,15 +719,24 @@ class MovingAverageConvergenceDivergenceSignal(CustomFactor): signal_period' : int > 0, < fast_period The window length for the signal line. Default is 9. - Returns - ------- - The EWMA of the difference between "fast" EWMA and "slow" EWMA line using - `signal_period` as span. + Notes + ----- + Unlike most Factors, MovingAverageConvergenceDivergence does not accept a + ``window_length`` parameter. ``window_length`` is inferred from + ``slow_period`` and ``signal_period``. """ - - inputs = [USEquityPricing.close] + inputs = (USEquityPricing.close,) + # We don't use the default form of `params` here because we want to + # dynamically calculate `window_length` from the period lengths in our + # __new__. params = ('fast_period', 'slow_period', 'signal_period') + @expect_bounded( + __funcname='MACDSignal', + fast_period=(1, None), # These must all be >= 1. + slow_period=(1, None), + signal_period=(1, None), + ) def __new__(cls, fast_period=12, slow_period=26, @@ -734,12 +744,13 @@ class MovingAverageConvergenceDivergenceSignal(CustomFactor): *args, **kwargs): - if signal_period <= 0: - raise ValueError("'signal_period' must be larger than 0.") - if slow_period <= fast_period or fast_period <= signal_period: + if slow_period <= fast_period: raise ValueError( - "'slow_period' must be larger than 'fast_period'." - "'fast_period' must be larger than 'signal_period'." + "'slow_period' must be greater than 'fast_period', but got\n" + "slow_period={slow}, fast_period={fast}".format( + slow=slow_period, + fast=fast_period, + ) ) return super(MovingAverageConvergenceDivergenceSignal, cls).__new__( @@ -753,10 +764,11 @@ class MovingAverageConvergenceDivergenceSignal(CustomFactor): def _ewma(self, data, length): decay_rate = 1.0 - (2.0 / (1.0 + length)) - return average(data, - axis=1, - weights=exponential_weights(length, decay_rate) - ) + return average( + data, + axis=1, + weights=exponential_weights(length, decay_rate) + ) def compute(self, today, assets, out, close, fast_period, slow_period, signal_period): @@ -778,19 +790,19 @@ class AnnualizedVolatility(CustomFactor): https://en.wikipedia.org/wiki/Volatility_(finance) The degree of variation of a series over time as measured by the standard - deviation of returns. + deviation of daily returns. **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. + annualization_factor : float, optional + The number of time units per year. Defaults is 252, the number of NYSE + trading days in a normal year. """ inputs = [Returns(window_length=2)] - params = {'annualization_factor': 252} + params = {'annualization_factor': 252.0} window_length = 252 def compute(self, today, assets, out, returns, annualization_factor):