diff --git a/tests/pipeline/test_alias.py b/tests/pipeline/test_alias.py new file mode 100644 index 00000000..3044fcdb --- /dev/null +++ b/tests/pipeline/test_alias.py @@ -0,0 +1,54 @@ +import numpy as np + +from zipline.testing.predicates import assert_equal +from zipline.pipeline import Classifier, Factor, Filter +from zipline.utils.numpy_utils import float64_dtype, int64_dtype + +from .base import BasePipelineTestCase + + +class WithAlias(object): + + def test_alias(self): + f = self.Term() + alias = f.alias('ayy lmao') + + f_values = np.random.RandomState(5).randn(5, 5) + + self.check_terms( + terms={ + 'f_alias': alias, + }, + expected={ + 'f_alias': f_values, + }, + initial_workspace={f: f_values}, + mask=self.build_mask(np.ones((5, 5))), + ) + + def test_repr(self): + assert_equal( + repr(self.Term().alias('ayy lmao')), + "Aliased%s(..., name='ayy lmao')" % self.Term.__base__.__name__, + ) + + +class TestFactorAlias(WithAlias, BasePipelineTestCase): + class Term(Factor): + dtype = float64_dtype + inputs = () + window_length = 0 + + +class TestFilterAlias(WithAlias, BasePipelineTestCase): + class Term(Filter): + inputs = () + window_length = 0 + + +class TestClassifierAlias(WithAlias, BasePipelineTestCase): + class Term(Classifier): + dtype = int64_dtype + inputs = () + window_length = 0 + missing_value = -1 diff --git a/tests/pipeline/test_factor.py b/tests/pipeline/test_factor.py index 3451d8f0..f020ae8b 100644 --- a/tests/pipeline/test_factor.py +++ b/tests/pipeline/test_factor.py @@ -20,14 +20,13 @@ from numpy import ( rot90, where, ) -from numpy.random import randn, RandomState, seed +from numpy.random import randn, seed from zipline.errors import UnknownRankMethod from zipline.lib.labelarray import LabelArray from zipline.lib.rank import masked_rankdata_2d from zipline.lib.normalize import naive_grouped_rowwise_apply as grouped_apply from zipline.pipeline import Classifier, Factor, Filter -from zipline.pipeline.term import Alias from zipline.pipeline.factors import ( Returns, RSI, @@ -1059,23 +1058,3 @@ class TestWindowSafety(TestCase): self.assertFalse(F().demean().window_safe) self.assertFalse(F(window_safe=False).demean().window_safe) self.assertTrue(F(window_safe=True).demean().window_safe) - - -class TestAlias(BasePipelineTestCase): - - def test_alias_factor(self): - f = F() - a = Alias(f) - - f_values = RandomState(5).randn(5, 5) - - self.check_terms( - terms={ - 'f_alias': a, - }, - expected={ - 'f_alias': f_values, - }, - initial_workspace={f: f_values}, - mask=self.build_mask(ones((5, 5))), - ) diff --git a/zipline/pipeline/classifiers/classifier.py b/zipline/pipeline/classifiers/classifier.py index 36b0a18a..1472cebd 100644 --- a/zipline/pipeline/classifiers/classifier.py +++ b/zipline/pipeline/classifiers/classifier.py @@ -24,6 +24,7 @@ from zipline.utils.numpy_utils import ( from ..filters import ArrayPredicate, NotNullFilter, NullFilter, NumExprFilter from ..mixins import ( + AliasedMixin, CustomTermMixin, DownsampledMixin, LatestMixin, @@ -323,6 +324,10 @@ class Classifier(RestrictedDTypeMixin, ComputableTerm): def _downsampled_type(self): return DownsampledMixin.make_downsampled_type(Classifier) + @classlazyval + def _aliased_type(self): + return AliasedMixin.make_aliased_type(Classifier) + class Everything(Classifier): """ diff --git a/zipline/pipeline/factors/factor.py b/zipline/pipeline/factors/factor.py index 3566abdc..793282d1 100644 --- a/zipline/pipeline/factors/factor.py +++ b/zipline/pipeline/factors/factor.py @@ -32,6 +32,7 @@ from zipline.pipeline.filters import ( NullFilter, ) from zipline.pipeline.mixins import ( + AliasedMixin, CustomTermMixin, DownsampledMixin, LatestMixin, @@ -1078,6 +1079,10 @@ class Factor(RestrictedDTypeMixin, ComputableTerm): def _downsampled_type(self): return DownsampledMixin.make_downsampled_type(Factor) + @classlazyval + def _aliased_type(self): + return AliasedMixin.make_aliased_type(Factor) + class NumExprFactor(NumericalExpression, Factor): """ diff --git a/zipline/pipeline/filters/filter.py b/zipline/pipeline/filters/filter.py index 89cd790a..7f17588b 100644 --- a/zipline/pipeline/filters/filter.py +++ b/zipline/pipeline/filters/filter.py @@ -24,6 +24,7 @@ from zipline.pipeline.expression import ( NumericalExpression, ) from zipline.pipeline.mixins import ( + AliasedMixin, CustomTermMixin, DownsampledMixin, LatestMixin, @@ -207,6 +208,10 @@ class Filter(RestrictedDTypeMixin, ComputableTerm): def _downsampled_type(self): return DownsampledMixin.make_downsampled_type(Filter) + @classlazyval + def _aliased_type(self): + return AliasedMixin.make_aliased_type(Filter) + class NumExprFilter(NumericalExpression, Filter): """ diff --git a/zipline/pipeline/mixins.py b/zipline/pipeline/mixins.py index a1825101..cd6048ad 100644 --- a/zipline/pipeline/mixins.py +++ b/zipline/pipeline/mixins.py @@ -20,6 +20,7 @@ from zipline.utils.control_flow import nullctx from zipline.utils.input_validation import expect_types from zipline.utils.sharedoc import ( format_docstring, + PIPELINE_ALIAS_DOC, PIPELINE_DOWNSAMPLING_FREQUENCY_DOC, ) from zipline.utils.pandas_utils import nearest_unequal_elements @@ -240,6 +241,72 @@ class LatestMixin(SingleInputMixin): ) +class AliasedMixin(SingleInputMixin): + """ + Mixin for aliased terms. + """ + def __new__(cls, term, name): + return super(AliasedMixin, cls).__new__( + cls, + inputs=(term,), + outputs=term.outputs, + window_length=0, + name=name, + dtype=term.dtype, + missing_value=term.missing_value, + ndim=term.ndim, + ) + + def _init(self, name, *args, **kwargs): + self.name = name + return super(AliasedMixin, self)._init(*args, **kwargs) + + @classmethod + def _static_identity(cls, name, *args, **kwargs): + return ( + super(AliasedMixin, cls)._static_identity(*args, **kwargs), + name, + ) + + def _compute(self, inputs, dates, assets, mask): + return inputs[0] + + def __repr__(self): + return '{type}(..., name={name!r})'.format( + type=type(self).__name__, + name=self.name, + ) + + @classmethod + def make_aliased_type(cls, other_base): + """ + Factory for making Aliased{Filter,Factor,Classifier}. + """ + docstring = dedent( + """ + A {t} that names another {t}. + + Parameters + ---------- + term : {t} + {{name}} + """ + ).format(t=other_base.__name__) + + doc = format_docstring( + owner_name=other_base.__name__, + docstring=docstring, + formatters={'name': PIPELINE_ALIAS_DOC}, + ) + + return type( + 'Aliased' + other_base.__name__, + (cls, other_base,), + {'__doc__': doc, + '__module__': other_base.__module__}, + ) + + class DownsampledMixin(StandardOutputs): """ Mixin for behavior shared by Downsampled{Factor,Filter,Classifier} diff --git a/zipline/pipeline/term.py b/zipline/pipeline/term.py index ce25ff92..b53d686e 100644 --- a/zipline/pipeline/term.py +++ b/zipline/pipeline/term.py @@ -39,6 +39,7 @@ from zipline.utils.numpy_utils import ( ) from zipline.utils.sharedoc import ( templated_docstring, + PIPELINE_ALIAS_DOC, PIPELINE_DOWNSAMPLING_FREQUENCY_DOC, ) @@ -602,7 +603,7 @@ class ComputableTerm(Term): fill_value=self.missing_value, ).values - def _downsampled_type(self): + def _downsampled_type(self, *args, **kwargs): """ The expression type to return from self.downsample(). """ @@ -623,6 +624,35 @@ class ComputableTerm(Term): """ return self._downsampled_type(term=self, frequency=frequency) + def _aliased_type(self, *args, **kwargs): + """ + The expression type to return from self.alias(). + """ + raise NotImplementedError( + "alias is not yet implemented " + "for instances of %s." % type(self).__name__ + ) + + @templated_docstring(name=PIPELINE_ALIAS_DOC) + def alias(self, name): + """ + Make a term from ``self`` that names the expression. + + Parameters + ---------- + {name} + + Returns + ------- + aliased : Aliased + ``self`` with a name. + + Notes + ----- + This is useful for giving a name to a numerical or boolean expression. + """ + return self._aliased_type(term=self, name=name) + def __repr__(self): return ( "{type}({inputs}, window_length={window_length})" @@ -696,26 +726,6 @@ class Slice(ComputableTerm): ) -class Alias(ComputableTerm): - """An alias for another computed term.""" - - @expect_types(term=ComputableTerm) - def __new__(cls, term): - return super(Alias, cls).__new__( - cls, - window_length=0, - dtype=term.dtype, - missing_value=term.missing_value, - window_safe=term.window_safe, - ndim=term.ndim, - domain=term.domain, - inputs=(term,), - ) - - def _compute(self, inputs, dates, assets, mask): - return inputs[0] - - def validate_dtype(termname, dtype, missing_value): """ Validate a `dtype` and `missing_value` passed to Term.__new__. diff --git a/zipline/utils/sharedoc.py b/zipline/utils/sharedoc.py index 9669366c..8c73d175 100644 --- a/zipline/utils/sharedoc.py +++ b/zipline/utils/sharedoc.py @@ -18,6 +18,13 @@ PIPELINE_DOWNSAMPLING_FREQUENCY_DOC = dedent( """ ) +PIPELINE_ALIAS_DOC = dedent( + """\ + name : str + The name to alias this term as. + """, +) + def pad_lines_after_first(prefix, s): """Apply a prefix to each line in s after the first."""