From f494d6f0d1ba5751d11d4a267d478288c2f66312 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Wed, 11 May 2016 21:37:12 -0400 Subject: [PATCH 01/11] BUG: Fix check that pipeline argument is hashable. Adds test coverage for the caes where it is not hashable. --- tests/pipeline/test_term.py | 44 ++++++++++++++++++++++++++++++------- zipline/pipeline/term.py | 7 +++--- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/tests/pipeline/test_term.py b/tests/pipeline/test_term.py index 502d6e48..797c95eb 100644 --- a/tests/pipeline/test_term.py +++ b/tests/pipeline/test_term.py @@ -27,6 +27,7 @@ from zipline.pipeline.data.testing import TestingDataSet from zipline.pipeline.term import AssetExists, NotSpecified from zipline.pipeline.expression import NUMEXPR_MATH_FUNCS from zipline.testing import parameter_space +from zipline.testing.predicates import assert_equal, assert_raises from zipline.utils.numpy_utils import ( bool_dtype, categorical_dtype, @@ -358,20 +359,20 @@ class ObjectIdentityTestCase(TestCase): method = getattr(f, funcname) self.assertIs(method(), method()) + class SomeFactorParameterized(SomeFactor): + params = ('a', 'b') + def test_parameterized_term(self): - class SomeFactorParameterized(SomeFactor): - params = ('a', 'b') - - f = SomeFactorParameterized(a=1, b=2) + f = self.SomeFactorParameterized(a=1, b=2) self.assertEqual(f.params, {'a': 1, 'b': 2}) - g = SomeFactorParameterized(a=1, b=3) - h = SomeFactorParameterized(a=2, b=2) + g = self.SomeFactorParameterized(a=1, b=3) + h = self.SomeFactorParameterized(a=2, b=2) self.assertDifferentObjects(f, g, h) - f2 = SomeFactorParameterized(a=1, b=2) - f3 = SomeFactorParameterized(b=2, a=1) + f2 = self.SomeFactorParameterized(a=1, b=2) + f3 = self.SomeFactorParameterized(b=2, a=1) self.assertSameObject(f, f2, f3) self.assertEqual(f.params['a'], 1) @@ -379,6 +380,33 @@ class ObjectIdentityTestCase(TestCase): self.assertEqual(f.window_length, SomeFactor.window_length) self.assertEqual(f.inputs, tuple(SomeFactor.inputs)) + def test_parameterized_term_non_hashable_arg(self): + with assert_raises(TypeError) as e: + self.SomeFactorParameterized(a=[], b=1) + assert_equal( + str(e.exception), + "SomeFactorParameterized expected a hashable value for parameter" + " 'a', but got [] instead.", + ) + + with assert_raises(TypeError) as e: + self.SomeFactorParameterized(a=1, b=[]) + assert_equal( + str(e.exception), + "SomeFactorParameterized expected a hashable value for parameter" + " 'b', but got [] instead.", + ) + + with assert_raises(TypeError) as e: + self.SomeFactorParameterized(a=[], b=[]) + assert_equal( + str(e.exception), + "SomeFactorParameterized expected a hashable value for parameter" + " 'a', but got [] instead.", + ) + + + def test_bad_input(self): class SomeFactor(Factor): diff --git a/zipline/pipeline/term.py b/zipline/pipeline/term.py index 1bffc3e7..389be97b 100644 --- a/zipline/pipeline/term.py +++ b/zipline/pipeline/term.py @@ -129,8 +129,7 @@ class Term(with_metaclass(ABCMeta, object)): value = kwargs.pop(key) # Check here that the value is hashable so that we fail here # instead of trying to hash the param values tuple later. - hash(key) - param_values.append(value) + hash(value) except KeyError: raise TypeError( "{typename} expected a keyword parameter {name!r}.".format( @@ -148,6 +147,8 @@ class Term(with_metaclass(ABCMeta, object)): value=value, ) ) + + param_values.append(value) return tuple(zip(cls.params, param_values)) @staticmethod @@ -240,7 +241,7 @@ class Term(with_metaclass(ABCMeta, object)): if hasattr(self, name): raise TypeError( "Parameter {name!r} conflicts with already-present" - "attribute with value {value!r}.".format( + " attribute with value {value!r}.".format( name=name, value=getattr(self, name), ) From 2297ace20c86db4bb3a409364f140c9085267439 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Wed, 11 May 2016 21:37:46 -0400 Subject: [PATCH 02/11] ENH: Adds BollingerBands factor. --- docs/source/appendix.rst | 3 + tests/pipeline/test_technical.py | 227 ++++++++++++++++++++++++++ zipline/pipeline/factors/__init__.py | 2 + zipline/pipeline/factors/technical.py | 29 ++++ 4 files changed, 261 insertions(+) create mode 100644 tests/pipeline/test_technical.py diff --git a/docs/source/appendix.rst b/docs/source/appendix.rst index 1ef2ded5..71e36586 100644 --- a/docs/source/appendix.rst +++ b/docs/source/appendix.rst @@ -74,6 +74,9 @@ Pipeline API .. autoclass:: zipline.pipeline.factors.AverageDollarVolume :members: +.. autoclass:: zipline.pipeline.factors.BollingerBands + :members: + .. autoclass:: zipline.pipeline.filters.Filter :members: __and__, __or__ :exclude-members: dtype diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py new file mode 100644 index 00000000..251cdb96 --- /dev/null +++ b/tests/pipeline/test_technical.py @@ -0,0 +1,227 @@ +from abc import ABCMeta, abstractmethod + +import numpy as np +from operator import itemgetter +import pandas as pd +from six import with_metaclass +import talib +from toolz import compose, excepts + +from zipline.lib.adjusted_array import AdjustedArray +from zipline.lib.adjustment import Float64Add +from zipline.pipeline import TermGraph +from zipline.pipeline.data import USEquityPricing +from zipline.pipeline.engine import SimplePipelineEngine +from zipline.pipeline.term import AssetExists +from zipline.pipeline.factors import BollingerBands +from zipline.testing import ExplodingObject, parameter_space +from zipline.testing.fixtures import WithAssetFinder, ZiplineTestCase +from zipline.testing.predicates import assert_equal + + +_meta = type('_', (type(ZiplineTestCase), ABCMeta), {}) + + +class WithTechnicalFactor(with_metaclass(_meta, WithAssetFinder)): + """ZiplineTestCase fixture for testing technical factors. + """ + ASSET_FINDER_EQUITY_SIDS = tuple(range(5)) + START_DATE = pd.Timestamp('2014-01-01', tz='utc') + + @classmethod + def init_class_fixtures(cls): + super(WithTechnicalFactor, cls).init_class_fixtures() + cls.ndays = ndays = 24 + cls.nassets = nassets = len(cls.ASSET_FINDER_EQUITY_SIDS) + cls.dates = dates = pd.date_range(cls.START_DATE, periods=ndays) + cls.assets = pd.Index(cls.asset_finder.sids) + cls.engine = SimplePipelineEngine( + lambda column: ExplodingObject(), + dates, + cls.asset_finder, + ) + cls.asset_exists = exists = np.full((ndays, nassets), True, dtype=bool) + cls.asset_exists_masked = masked = exists.copy() + masked[:, -1] = False + + def run_graph(self, graph, initial_workspace, mask_sid): + initial_workspace.setdefault( + AssetExists(), + self.asset_exists_masked if mask_sid else self.asset_exists, + ) + return self.engine.compute_chunk( + graph, + self.dates, + self.assets, + initial_workspace, + ) + + @abstractmethod + def test_without_adjustments(self): + raise NotImplementedError('test_without_adjustments') + + @abstractmethod + def test_with_adjustments(self): + raise NotImplementedError('test_with_adjustments') + + +class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): + cases = parameter_space( + window_length={5, 10, 20}, + k={1.5, 2, 2.5}, + mask_sid={True, False}, + ) + + @classmethod + def init_class_fixtures(cls): + super(BollingerBandsTestCase, cls).init_class_fixtures() + cls._closes = closes = ( + np.arange(cls.ndays, dtype=float)[:, np.newaxis] + + np.arange(cls.nassets, dtype=float) * 100 + ) + cls._closes_masked = masked = closes.copy() + masked[:, -1] = np.nan + + def closes(self, masked): + return self._closes_masked if masked else self._closes + + def _allnans(self, e): + """Handler for toolz.excepts that returns three all nan arrays. + """ + nans = np.full(self.ndays, np.nan) + return nans, nans, nans + + def expected(self, window_length, k, closes): + """Compute the expected data (without adjustments) for the given + window, k, and closes array. + + This uses talib.BBANDS to generate the expected data. + """ + return map( + # Stack all of our uppers, middles, lowers into three 2d arrays + # whose columns are the sids. After that, slice off only the + # window care about. + compose( + itemgetter(np.s_[window_length - 1:]), + np.column_stack, + ), + # Take our sequence of [(uppers, middles, lowers)] per sid and + # turn it into three sequences of: + # uppers for all sids, middles for all sids, lowers for all sids. + zip(*( + # talib breaks when the input array is all nan and raises + # an instance of Exception. We catch that here and return + # three all nan arrays instead. + excepts(Exception, talib.BBANDS, self._allnans)( + closes[:, n], + window_length, + k, + k, + ) + for n in range(self.nassets) + )), + ) + + @cases + def test_without_adjustments(self, window_length, k, mask_sid): + closes = self.closes(mask_sid) + result = self.run_graph( + TermGraph({ + 'f': BollingerBands( + window_length=window_length, + k=k, + ), + }), + initial_workspace={ + USEquityPricing.close: AdjustedArray( + closes, + np.full_like(closes, True, dtype=bool), + {}, + np.nan, + ), + }, + mask_sid=mask_sid, + )['f'] + + expected_upper, expected_middle, expected_lower = self.expected( + window_length, + k, + closes, + ) + + assert_equal( + result.upper, + expected_upper, + ) + assert_equal( + result.middle, + expected_middle, + ) + assert_equal( + result.lower, + expected_lower, + ) + + @cases + def test_with_adjustments(self, window_length, k, mask_sid): + closes = self.closes(mask_sid) + adjustment_offset = 5 + adjustment_idx = self.ndays - adjustment_offset + result = self.run_graph( + TermGraph({ + 'f': BollingerBands( + window_length=window_length, + k=k, + ), + }), + initial_workspace={ + USEquityPricing.close: AdjustedArray( + closes, + np.full_like(closes, True, dtype=bool), + { + adjustment_idx: [ + Float64Add( + first_row=0, + last_row=adjustment_idx, + first_col=0, + last_col=self.nassets - 1, + value=1000, + ), + ], + }, + np.nan, + ), + }, + mask_sid=mask_sid, + )['f'] + + # Get the uppers, middles, and lowers without the adjustment applied. + bases = self.expected(window_length, k, closes) + adjusted_closes = closes.copy() + adjusted_closes[:adjustment_idx + 1] += 1000 + # Get the uppers, middles, and lowers with the adjustment applied. + adjusted = self.expected(window_length, k, adjusted_closes) + + # Create the actual expected data by using the unadjusted results up + # to the adjument offset, then use the adjusted data for all indices + # past the adjustment offset. + expected_upper, expected_middle, expected_lower = ( + np.vstack(( + base[:-adjustment_offset], + adjusted[-adjustment_offset:], + )) + for base, adjusted in zip(bases, adjusted) + ) + + assert_equal( + result.upper, + expected_upper, + ) + assert_equal( + result.middle, + expected_middle, + ) + assert_equal( + result.lower, + expected_lower, + ) diff --git a/zipline/pipeline/factors/__init__.py b/zipline/pipeline/factors/__init__.py index d6d32b98..2f2aa024 100644 --- a/zipline/pipeline/factors/__init__.py +++ b/zipline/pipeline/factors/__init__.py @@ -16,6 +16,7 @@ from .events import ( ) from .technical import ( AverageDollarVolume, + BollingerBands, EWMA, EWMSTD, ExponentialWeightedMovingAverage, @@ -29,6 +30,7 @@ from .technical import ( ) __all__ = [ + 'BollingerBands', 'BusinessDaysSince13DFilingsDate', 'BusinessDaysSinceCashBuybackAuth', 'BusinessDaysSinceDividendAnnouncement', diff --git a/zipline/pipeline/factors/technical.py b/zipline/pipeline/factors/technical.py index f67471fc..0277b55a 100644 --- a/zipline/pipeline/factors/technical.py +++ b/zipline/pipeline/factors/technical.py @@ -29,6 +29,7 @@ from zipline.utils.math_utils import ( nanargmax, nanmax, nanmean, + nanstd, nansum, ) from .factor import CustomFactor @@ -396,3 +397,31 @@ class ExponentialWeightedMovingStdDev(_ExponentialWeightedFactor): # Convenience aliases. EWMA = ExponentialWeightedMovingAverage EWMSTD = ExponentialWeightedMovingStdDev + + +class BollingerBands(CustomFactor): + """ + Bollinger Bands + + **Default Inputs:** :data:`zipline.pipeline.data.USEquityPricing.close` + + Parameters + ---------- + inputs : length-1 iterable[BoundColumn] + The expression over which to compute bollinger bands. + window_length : int > 0 + Length of the lookback window over which to compute the bollinger + bands. + k : float + The number of standard deviations to add or subtract to create the + upper and lower bands. + """ + params = ('k',) + inputs = (USEquityPricing.close,) + outputs = 'lower', 'middle', 'upper' + + def compute(self, today, assets, out, close, k): + difference = k * nanstd(close, axis=0) + out.middle = middle = nanmean(close, axis=0) + out.upper = middle + difference + out.lower = middle - difference From 9b7673114305035256259f827a06084544084a8a Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Thu, 12 May 2016 15:57:50 -0400 Subject: [PATCH 03/11] ENH: adds with_metaclasses and tests for metautils --- tests/utils/test_metautils.py | 91 +++++++++++++++++++++++++++++++++++ zipline/testing/predicates.py | 28 +++++++++++ zipline/utils/metautils.py | 37 +++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 tests/utils/test_metautils.py diff --git a/tests/utils/test_metautils.py b/tests/utils/test_metautils.py new file mode 100644 index 00000000..ad864d9e --- /dev/null +++ b/tests/utils/test_metautils.py @@ -0,0 +1,91 @@ +from zipline.testing.fixtures import ZiplineTestCase +from zipline.testing.predicates import ( + assert_equal, + assert_is, + assert_is_instance, + assert_is_subclass, + assert_true, +) +from zipline.utils.metautils import compose_types, with_metaclasses + + +class C(object): + @staticmethod + def f(): + return 'C.f' + + def delegate(self): + return 'C.delegate', super(C, self).delegate() + + +class D(object): + @staticmethod + def f(): + return 'D.f' + + @staticmethod + def g(): + return 'D.g' + + def delegate(self): + return 'D.delegate' + + +class ComposeTypesTestCase(ZiplineTestCase): + + def test_identity(self): + assert_is( + compose_types(C), + C, + msg='compose_types of a single class should be identity', + ) + + def test_compose(self): + composed = compose_types(C, D) + + assert_is_subclass(composed, C) + assert_is_subclass(composed, D) + + def test_compose_mro(self): + composed = compose_types(C, D) + + assert_equal(composed.f(), C.f()) + assert_equal(composed.g(), D.g()) + + assert_equal(composed().delegate(), ('C.delegate', 'D.delegate')) + + +class M(type): + def __new__(mcls, name, bases, dict_): + dict_['M'] = True + return super(M, mcls).__new__(mcls, name, bases, dict_) + + +class N(type): + def __new__(mcls, name, bases, dict_): + dict_['N'] = True + return super(N, mcls).__new__(mcls, name, bases, dict_) + + +class WithMetaclassesTestCase(ZiplineTestCase): + def test_with_metaclasses_no_subclasses(self): + class E(with_metaclasses((M, N))): + pass + + assert_true(E.M) + assert_true(E.N) + + assert_is_instance(E, M) + assert_is_instance(E, N) + + def test_with_metaclasses_with_subclasses(self): + class E(with_metaclasses((M, N), C, D)): + pass + + assert_true(E.M) + assert_true(E.N) + + assert_is_instance(E, M) + assert_is_instance(E, N) + assert_is_subclass(E, C) + assert_is_subclass(E, D) diff --git a/zipline/testing/predicates.py b/zipline/testing/predicates.py index 93f3610e..b83bdbf8 100644 --- a/zipline/testing/predicates.py +++ b/zipline/testing/predicates.py @@ -141,6 +141,34 @@ def _fmt_msg(msg): return msg + '\n' +def _safe_cls_name(cls): + try: + return cls.__name__ + except AttributeError: + return repr(cls) + + +def assert_is_subclass(subcls, cls, msg=''): + """Assert that ``subcls`` is a subclass of ``cls``. + + Parameters + ---------- + subcls : type + The type to check. + cls : type + The type to check ``subcls`` against. + msg : str, optional + An extra assertion message to print if this fails. + """ + assert issubclass(subcls, cls), ( + '%s is not a subclass of %s\n%s' % ( + _safe_cls_name(subcls), + _safe_cls_name(cls), + msg, + ) + ) + + @dispatch(object, object) def assert_equal(result, expected, path=(), msg='', **kwargs): """Assert that two objects are equal using the ``==`` operator. diff --git a/zipline/utils/metautils.py b/zipline/utils/metautils.py index fa0dd0ed..a2f86080 100644 --- a/zipline/utils/metautils.py +++ b/zipline/utils/metautils.py @@ -1,7 +1,9 @@ from operator import attrgetter +import six -def compose_types(a, b, *cs): + +def compose_types(a, *cs): """Compose multiple classes together. Parameters @@ -66,9 +68,40 @@ def compose_types(a, b, *cs): Always using ``super()`` to dispatch to your superclass is best practices anyways so most classes should compose without much special considerations. """ - mcls = (a, b) + cs + if not cs: + # if there are no types to compose then just return the single type + return a + + mcls = (a,) + cs return type( 'compose_types(%s)' % ', '.join(map(attrgetter('__name__'), mcls)), mcls, {}, ) + + +def with_metaclasses(metaclasses, *bases): + """Make a class inheriting from ``bases`` whose metaclass inherits from + all of ``metaclasses``. + + Like :func:`six.with_metaclass`, but allows multiple metaclasses. + + Parameters + ---------- + metaclasses : iterable[type] + A tuple of types to use as metaclasses. + *bases : tuple[type] + A tuple of types to use as bases. + + Returns + ------- + base : type + A subtype of ``bases`` whose metaclass is a subtype of ``metaclasses``. + + Notes + ----- + The metaclasses must be written to support cooperative multiple + inheritance. This means that they must delegate all calls to ``super()`` + instead of inlining their super class by name. + """ + return six.with_metaclass(compose_types(*metaclasses), *bases) From a345e6f3f5225999670597ba4057f70c9423fdb6 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Thu, 12 May 2016 15:58:42 -0400 Subject: [PATCH 04/11] TST: Clean up metaclass usage in fixtures --- tests/pipeline/test_technical.py | 8 ++------ zipline/testing/fixtures.py | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index 251cdb96..5ac08fd6 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -1,9 +1,8 @@ -from abc import ABCMeta, abstractmethod +from abc import abstractmethod import numpy as np from operator import itemgetter import pandas as pd -from six import with_metaclass import talib from toolz import compose, excepts @@ -19,10 +18,7 @@ from zipline.testing.fixtures import WithAssetFinder, ZiplineTestCase from zipline.testing.predicates import assert_equal -_meta = type('_', (type(ZiplineTestCase), ABCMeta), {}) - - -class WithTechnicalFactor(with_metaclass(_meta, WithAssetFinder)): +class WithTechnicalFactor(WithAssetFinder): """ZiplineTestCase fixture for testing technical factors. """ ASSET_FINDER_EQUITY_SIDS = tuple(range(5)) diff --git a/zipline/testing/fixtures.py b/zipline/testing/fixtures.py index 77c33671..9363301d 100644 --- a/zipline/testing/fixtures.py +++ b/zipline/testing/fixtures.py @@ -12,7 +12,6 @@ import numpy as np import pandas as pd import responses - from .core import ( create_daily_bar_data, create_minute_bar_data, @@ -37,7 +36,7 @@ from ..finance.trading import TradingEnvironment from ..utils import tradingcalendar, factory from ..utils.classproperty import classproperty from ..utils.final import FinalMeta, final -from ..utils.metautils import compose_types +from ..utils.metautils import with_metaclasses from .core import tmp_asset_finder, make_simple_equity_info, gen_calendars from zipline.pipeline import Pipeline, SimplePipelineEngine from zipline.pipeline.loaders.testing import make_seeded_random_loader @@ -818,8 +817,8 @@ class WithAdjustmentReader(WithBcolzDailyBarReader): cls.adjustment_reader = SQLiteAdjustmentReader(conn) -class WithPipelineEventDataLoader(with_metaclass( - compose_types(ABCMeta, type(ZiplineTestCase)), WithAssetFinder)): +class WithPipelineEventDataLoader( + with_metaclasses((type(ZiplineTestCase), ABCMeta), WithAssetFinder)): """ ZiplineTestCase mixin providing common test methods/behaviors for event data loaders. From 78db90a858e0881d60a72d491e58a31eb23b2f35 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Thu, 12 May 2016 15:59:36 -0400 Subject: [PATCH 05/11] STY: flake8 --- tests/pipeline/test_term.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/pipeline/test_term.py b/tests/pipeline/test_term.py index 797c95eb..a7422f0b 100644 --- a/tests/pipeline/test_term.py +++ b/tests/pipeline/test_term.py @@ -405,8 +405,6 @@ class ObjectIdentityTestCase(TestCase): " 'a', but got [] instead.", ) - - def test_bad_input(self): class SomeFactor(Factor): From c128b69a91d95bd7cf0cecf52448c918616f3e46 Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Thu, 12 May 2016 17:00:30 -0400 Subject: [PATCH 06/11] STY: get Scott to stop yelling at me --- tests/pipeline/test_technical.py | 139 +++++++------------------------ 1 file changed, 29 insertions(+), 110 deletions(-) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index 5ac08fd6..c2b1dfc6 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -1,13 +1,8 @@ -from abc import abstractmethod - import numpy as np -from operator import itemgetter import pandas as pd import talib -from toolz import compose, excepts from zipline.lib.adjusted_array import AdjustedArray -from zipline.lib.adjustment import Float64Add from zipline.pipeline import TermGraph from zipline.pipeline.data import USEquityPricing from zipline.pipeline.engine import SimplePipelineEngine @@ -52,22 +47,8 @@ class WithTechnicalFactor(WithAssetFinder): initial_workspace, ) - @abstractmethod - def test_without_adjustments(self): - raise NotImplementedError('test_without_adjustments') - - @abstractmethod - def test_with_adjustments(self): - raise NotImplementedError('test_with_adjustments') - class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): - cases = parameter_space( - window_length={5, 10, 20}, - k={1.5, 2, 2.5}, - mask_sid={True, False}, - ) - @classmethod def init_class_fixtures(cls): super(BollingerBandsTestCase, cls).init_class_fixtures() @@ -81,45 +62,47 @@ class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): def closes(self, masked): return self._closes_masked if masked else self._closes - def _allnans(self, e): - """Handler for toolz.excepts that returns three all nan arrays. - """ - nans = np.full(self.ndays, np.nan) - return nans, nans, nans - def expected(self, window_length, k, closes): """Compute the expected data (without adjustments) for the given window, k, and closes array. This uses talib.BBANDS to generate the expected data. """ - return map( - # Stack all of our uppers, middles, lowers into three 2d arrays - # whose columns are the sids. After that, slice off only the - # window care about. - compose( - itemgetter(np.s_[window_length - 1:]), - np.column_stack, - ), - # Take our sequence of [(uppers, middles, lowers)] per sid and - # turn it into three sequences of: - # uppers for all sids, middles for all sids, lowers for all sids. - zip(*( - # talib breaks when the input array is all nan and raises - # an instance of Exception. We catch that here and return - # three all nan arrays instead. - excepts(Exception, talib.BBANDS, self._allnans)( + lower_cols = [] + middle_cols = [] + upper_cols = [] + for n in range(self.nassets): + try: + upper, middle, lower = talib.BBANDS( closes[:, n], window_length, k, k, ) - for n in range(self.nassets) - )), - ) + except Exception: + # If the input array is all nan then talib raises an instance + # of Exception. + upper, middle, lower = [np.full(self.ndays, np.nan)] * 3 - @cases - def test_without_adjustments(self, window_length, k, mask_sid): + upper_cols.append(upper) + middle_cols.append(middle) + lower_cols.append(lower) + + # Stack all of our uppers, middles, lowers into three 2d arrays + # whose columns are the sids. After that, slice off only the + # rows we care about. + where = np.s_[window_length - 1:] + uppers = np.column_stack(upper_cols)[where] + middles = np.column_stack(middle_cols)[where] + lowers = np.column_stack(lower_cols)[where] + return uppers, middles, lowers + + @parameter_space( + window_length={5, 10, 20}, + k={1.5, 2, 2.5}, + mask_sid={True, False}, + ) + def test_bollinger_bands(self, window_length, k, mask_sid): closes = self.closes(mask_sid) result = self.run_graph( TermGraph({ @@ -157,67 +140,3 @@ class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): result.lower, expected_lower, ) - - @cases - def test_with_adjustments(self, window_length, k, mask_sid): - closes = self.closes(mask_sid) - adjustment_offset = 5 - adjustment_idx = self.ndays - adjustment_offset - result = self.run_graph( - TermGraph({ - 'f': BollingerBands( - window_length=window_length, - k=k, - ), - }), - initial_workspace={ - USEquityPricing.close: AdjustedArray( - closes, - np.full_like(closes, True, dtype=bool), - { - adjustment_idx: [ - Float64Add( - first_row=0, - last_row=adjustment_idx, - first_col=0, - last_col=self.nassets - 1, - value=1000, - ), - ], - }, - np.nan, - ), - }, - mask_sid=mask_sid, - )['f'] - - # Get the uppers, middles, and lowers without the adjustment applied. - bases = self.expected(window_length, k, closes) - adjusted_closes = closes.copy() - adjusted_closes[:adjustment_idx + 1] += 1000 - # Get the uppers, middles, and lowers with the adjustment applied. - adjusted = self.expected(window_length, k, adjusted_closes) - - # Create the actual expected data by using the unadjusted results up - # to the adjument offset, then use the adjusted data for all indices - # past the adjustment offset. - expected_upper, expected_middle, expected_lower = ( - np.vstack(( - base[:-adjustment_offset], - adjusted[-adjustment_offset:], - )) - for base, adjusted in zip(bases, adjusted) - ) - - assert_equal( - result.upper, - expected_upper, - ) - assert_equal( - result.middle, - expected_middle, - ) - assert_equal( - result.lower, - expected_lower, - ) From cbd4ea36bca615f41e733df3518767fb24b6b9fc Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Fri, 13 May 2016 14:31:33 -0400 Subject: [PATCH 07/11] STY: No need for these to be vertical. --- tests/pipeline/test_technical.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index c2b1dfc6..335b90c9 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -128,15 +128,6 @@ class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): closes, ) - assert_equal( - result.upper, - expected_upper, - ) - assert_equal( - result.middle, - expected_middle, - ) - assert_equal( - result.lower, - expected_lower, - ) + assert_equal(result.upper, expected_upper) + assert_equal(result.middle, expected_middle) + assert_equal(result.lower, expected_lower) From 2f906656763c4057153832ee6783121d66cf7955 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Fri, 13 May 2016 14:31:58 -0400 Subject: [PATCH 08/11] TEST: Add a test for bbands output ordering. --- tests/pipeline/test_technical.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index 335b90c9..17b7f015 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -131,3 +131,10 @@ class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): assert_equal(result.upper, expected_upper) assert_equal(result.middle, expected_middle) assert_equal(result.lower, expected_lower) + + def test_bollinger_bands_output_ordering(self): + bbands = BollingerBands(window_length=5, k=2) + lower, middle, upper = bbands + self.assertIs(lower, bbands.lower) + self.assertIs(middle, bbands.middle) + self.assertIs(upper, bbands.upper) From c4b69d6223bd6257b932cab7f90a4fa8abb66273 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Fri, 13 May 2016 14:32:21 -0400 Subject: [PATCH 09/11] TEST: Don't mask unexpected exceptions from TALIB. We know when we expect the error to be raised. --- tests/pipeline/test_technical.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index 17b7f015..597aa84c 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -72,17 +72,17 @@ class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): middle_cols = [] upper_cols = [] for n in range(self.nassets): - try: + close_col = closes[:, n] + if np.isnan(close_col).all(): + # ta-lib doesn't deal well with all nans. + upper, middle, lower = [np.full(self.ndays, np.nan)] * 3 + else: upper, middle, lower = talib.BBANDS( closes[:, n], window_length, k, k, ) - except Exception: - # If the input array is all nan then talib raises an instance - # of Exception. - upper, middle, lower = [np.full(self.ndays, np.nan)] * 3 upper_cols.append(upper) middle_cols.append(middle) From f4d96e065a841e7a7a68fb6f1ccbc6ae0a027999 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Fri, 13 May 2016 14:44:26 -0400 Subject: [PATCH 10/11] TEST/PERF: Don't slice twice. --- tests/pipeline/test_technical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pipeline/test_technical.py b/tests/pipeline/test_technical.py index 597aa84c..313337e4 100644 --- a/tests/pipeline/test_technical.py +++ b/tests/pipeline/test_technical.py @@ -78,7 +78,7 @@ class BollingerBandsTestCase(WithTechnicalFactor, ZiplineTestCase): upper, middle, lower = [np.full(self.ndays, np.nan)] * 3 else: upper, middle, lower = talib.BBANDS( - closes[:, n], + close_col, window_length, k, k, From fa15b49a3262944e44b222939184443e855ba06e Mon Sep 17 00:00:00 2001 From: Joe Jevnik Date: Fri, 13 May 2016 15:36:02 -0400 Subject: [PATCH 11/11] DOC: update whatsnew --- docs/source/whatsnew/1.0.0.txt | 9 ++++++++- zipline/pipeline/factors/technical.py | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/source/whatsnew/1.0.0.txt b/docs/source/whatsnew/1.0.0.txt index 64e73a74..2aa6de05 100644 --- a/docs/source/whatsnew/1.0.0.txt +++ b/docs/source/whatsnew/1.0.0.txt @@ -127,6 +127,10 @@ Enhancements ``element_of`` is defined for all classifiers. The remaining methods are only defined for strings. (:issue:`1174`) +* Added :class:`~zipline.pipeline.factors.BollingerBands` factor. This factor + implements the Bollinger Bands technical indicator: + https://en.wikipedia.org/wiki/Bollinger_Bands (:issue:`1199`). + Experimental Features ~~~~~~~~~~~~~~~~~~~~~ @@ -165,7 +169,10 @@ None Documentation ~~~~~~~~~~~~~ -None +* Updated documentation for the API methods (:issue:`1188`). + +* Updated release process to mention that docs should be built with python 3 + (:issue:`1188`). Miscellaneous ~~~~~~~~~~~~~ diff --git a/zipline/pipeline/factors/technical.py b/zipline/pipeline/factors/technical.py index 0277b55a..74b02c2b 100644 --- a/zipline/pipeline/factors/technical.py +++ b/zipline/pipeline/factors/technical.py @@ -401,7 +401,8 @@ EWMSTD = ExponentialWeightedMovingStdDev class BollingerBands(CustomFactor): """ - Bollinger Bands + Bollinger Bands technical indicator. + https://en.wikipedia.org/wiki/Bollinger_Bands **Default Inputs:** :data:`zipline.pipeline.data.USEquityPricing.close`