Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New factors #910

Merged
merged 4 commits into from
Dec 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/source/appendix.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ Pipeline API
.. autoclass:: zipline.pipeline.factors.WeightedAverageValue
:members:

.. autoclass:: zipline.pipeline.factors.ExponentialWeightedMovingAverage
:members:

.. autoclass:: zipline.pipeline.factors.ExponentialWeightedStandardDeviation
:members:

.. autofunction:: zipline.pipeline.factors.DollarVolume

.. autoclass:: zipline.pipeline.filters.Filter
:members: __and__, __or__
:exclude-members: dtype
Expand Down
15 changes: 13 additions & 2 deletions docs/source/whatsnew/0.8.4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ Highlights
* :class:`~zipline.assets.assets.AssetFinder` speedups (:issue:`830` and
:issue:`817`).


Enhancements
~~~~~~~~~~~~

Expand Down Expand Up @@ -53,14 +52,25 @@ Enhancements
calculates the percent change in close price over the given
window_length. (:issue:`884`).

* Added a new built-in factor:
:class:`~zipline.pipeline.factors.DollarVolume`. (:issue:`910`).

* Added :class:`~zipline.pipeline.factors.ExponentialWeightedMovingAverage` and
:class:`~zipline.pipeline.factors.ExponentialWeightedStandardDeviation`
factors. (:issue:`910`).

Experimental Features
~~~~~~~~~~~~~~~~~~~~~

.. warning::

Experimental features are subject to change.

None
* Added support for parameterized ``Factor`` subclasses. Factors may specify
``params`` as a class-level attribute containing a tuple of parameter names.
These values are then accepted by the constructor and forwarded by name to
the factor's ``compute`` function. This API is experimental, and may change
in future releases.

Bug Fixes
~~~~~~~~~
Expand All @@ -71,6 +81,7 @@ Bug Fixes

* Fixes an error raised in calculating beta when benchmark data were sparse.
Instead `numpy.nan` is returned (:issue:`859`).

* Fixed an issue pickling :func:`~zipline.utils.sentinel.sentinel` objects
(:issue:`872`).

Expand Down
190 changes: 190 additions & 0 deletions tests/pipeline/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@
from unittest import TestCase
from itertools import product

from nose_parameterized import parameterized
from numpy import (
arange,
array,
full,
nan,
tile,
zeros,
float32,
concatenate,
log,
)
from numpy.testing import assert_almost_equal
from pandas import (
DataFrame,
date_range,
ewma,
ewmstd,
Int64Index,
MultiIndex,
rolling_apply,
rolling_mean,
Series,
Timestamp,
Expand All @@ -28,6 +35,7 @@
from pandas.util.testing import assert_frame_equal
from six import iteritems, itervalues
from testfixtures import TempDirectory
from toolz import merge

from zipline.data.us_equity_pricing import BcolzDailyBarReader
from zipline.finance.trading import TradingEnvironment
Expand All @@ -46,6 +54,11 @@
from zipline.pipeline.engine import SimplePipelineEngine
from zipline.pipeline import CustomFactor
from zipline.pipeline.factors import (
DollarVolume,
EWMA,
EWMSTD,
ExponentialWeightedMovingAverage,
ExponentialWeightedStandardDeviation,
MaxDrawdown,
SimpleMovingAverage,
)
Expand Down Expand Up @@ -767,3 +780,180 @@ def test_drawdown(self):
result = results['drawdown'].unstack()

assert_frame_equal(expected, result)


class ParameterizedFactorTestCase(TestCase):
@classmethod
def setUpClass(cls):
cls.env = TradingEnvironment()
day = cls.env.trading_day

cls.sids = sids = Int64Index([1, 2, 3])
cls.dates = dates = date_range(
'2015-02-01',
'2015-02-28',
freq=day,
tz='UTC',
)

asset_info = make_simple_equity_info(
cls.sids,
start_date=Timestamp('2015-01-31', tz='UTC'),
end_date=Timestamp('2015-03-01', tz='UTC'),
)
cls.env.write_data(equities_df=asset_info)
cls.asset_finder = cls.env.asset_finder

cls.raw_data = DataFrame(
data=arange(len(dates) * len(sids), dtype=float).reshape(
len(dates), len(sids),
),
index=dates,
columns=cls.asset_finder.retrieve_all(sids),
)

close_loader = DataFrameLoader(USEquityPricing.close, cls.raw_data)
volume_loader = DataFrameLoader(
USEquityPricing.volume,
cls.raw_data * 2,
)

cls.engine = SimplePipelineEngine(
{
USEquityPricing.close: close_loader,
USEquityPricing.volume: volume_loader,
}.__getitem__,
cls.dates,
cls.asset_finder,
)

@classmethod
def tearDownClass(cls):
del cls.env
del cls.asset_finder

def expected_ewma(self, window_length, decay_rate):
alpha = 1 - decay_rate
span = (2 / alpha) - 1
return rolling_apply(
self.raw_data,
window_length,
lambda window: ewma(window, span=span)[-1],
)[window_length:]

def expected_ewmstd(self, window_length, decay_rate):
alpha = 1 - decay_rate
span = (2 / alpha) - 1
return rolling_apply(
self.raw_data,
window_length,
lambda window: ewmstd(window, span=span)[-1],
)[window_length:]

@parameterized.expand([
(3,),
(5,),
])
def test_ewm_stats(self, window_length):

def ewma_name(decay_rate):
return 'ewma_%s' % decay_rate

def ewmstd_name(decay_rate):
return 'ewmstd_%s' % decay_rate

decay_rates = [0.25, 0.5, 0.75]
ewmas = {
ewma_name(decay_rate): EWMA(
inputs=(USEquityPricing.close,),
window_length=window_length,
decay_rate=decay_rate,
)
for decay_rate in decay_rates
}

ewmstds = {
ewmstd_name(decay_rate): EWMSTD(
inputs=(USEquityPricing.close,),
window_length=window_length,
decay_rate=decay_rate,
)
for decay_rate in decay_rates
}

all_results = self.engine.run_pipeline(
Pipeline(columns=merge(ewmas, ewmstds)),
self.dates[window_length],
self.dates[-1],
)

for decay_rate in decay_rates:
ewma_result = all_results[ewma_name(decay_rate)].unstack()
ewma_expected = self.expected_ewma(window_length, decay_rate)
assert_frame_equal(ewma_result, ewma_expected)

ewmstd_result = all_results[ewmstd_name(decay_rate)].unstack()
ewmstd_expected = self.expected_ewmstd(window_length, decay_rate)
assert_frame_equal(ewmstd_result, ewmstd_expected)

@staticmethod
def decay_rate_to_span(decay_rate):
alpha = 1 - decay_rate
return (2 / alpha) - 1

@staticmethod
def decay_rate_to_com(decay_rate):
alpha = 1 - decay_rate
return (1 / alpha) - 1

@staticmethod
def decay_rate_to_halflife(decay_rate):
return log(.5) / log(decay_rate)

def ewm_cases():
return product([EWMSTD, EWMA], [3, 5, 10])

@parameterized.expand(ewm_cases())
def test_from_span(self, type_, span):
from_span = type_.from_span(
inputs=[USEquityPricing.close],
window_length=20,
span=span,
)
implied_span = self.decay_rate_to_span(from_span.params['decay_rate'])
assert_almost_equal(span, implied_span)

@parameterized.expand(ewm_cases())
def test_from_halflife(self, type_, halflife):
from_hl = EWMA.from_halflife(
inputs=[USEquityPricing.close],
window_length=20,
halflife=halflife,
)
implied_hl = self.decay_rate_to_halflife(from_hl.params['decay_rate'])
assert_almost_equal(halflife, implied_hl)

@parameterized.expand(ewm_cases())
def test_from_com(self, type_, com):
from_com = EWMA.from_center_of_mass(
inputs=[USEquityPricing.close],
window_length=20,
center_of_mass=com,
)
implied_com = self.decay_rate_to_com(from_com.params['decay_rate'])
assert_almost_equal(com, implied_com)

del ewm_cases

def test_ewm_aliasing(self):
self.assertIs(ExponentialWeightedMovingAverage, EWMA)
self.assertIs(ExponentialWeightedStandardDeviation, EWMSTD)

def test_dollar_volume(self):
results = self.engine.run_pipeline(
Pipeline(columns={'dv': DollarVolume()}),
self.dates[0],
self.dates[-1],
)['dv'].unstack()
expected = (self.raw_data ** 2) * 2
assert_frame_equal(results, expected)
5 changes: 4 additions & 1 deletion tests/pipeline/test_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from zipline.errors import UnknownRankMethod
from zipline.lib.rank import masked_rankdata_2d
from zipline.pipeline import Factor, Filter, TermGraph
from zipline.pipeline.factors import RSI, Returns
from zipline.pipeline.factors import (
Returns,
RSI,
)
from zipline.utils.test_utils import check_allclose, check_arrays
from zipline.utils.numpy_utils import datetime64ns_dtype, float64_dtype, np_NaT

Expand Down
29 changes: 29 additions & 0 deletions tests/pipeline/test_term.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for Term.
"""
from collections import Counter
from itertools import product
from unittest import TestCase

Expand Down Expand Up @@ -169,6 +170,13 @@ def assertSameObject(self, *objs):
for obj in objs:
self.assertIs(first, obj)

def assertDifferentObjects(self, *objs):
id_counts = Counter(map(id, objs))
((most_common_id, count),) = id_counts.most_common(1)
if count > 1:
dupe = [o for o in objs if id(o) == most_common_id][0]
self.fail("%s appeared %d times in %s" % (dupe, count, objs))

def test_instance_caching(self):

self.assertSameObject(*gen_equivalent_factors())
Expand Down Expand Up @@ -259,6 +267,27 @@ def test_instance_caching_math_funcs(self):
method = getattr(f, funcname)
self.assertIs(method(), method())

def test_parameterized_term(self):

class SomeFactorParameterized(SomeFactor):
params = ('a', 'b')

f = 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)
self.assertDifferentObjects(f, g, h)

f2 = SomeFactorParameterized(a=1, b=2)
f3 = SomeFactorParameterized(b=2, a=1)
self.assertSameObject(f, f2, f3)

self.assertEqual(f.params['a'], 1)
self.assertEqual(f.params['b'], 2)
self.assertEqual(f.window_length, SomeFactor.window_length)
self.assertEqual(f.inputs, tuple(SomeFactor.inputs))

def test_bad_input(self):

class SomeFactor(Factor):
Expand Down
11 changes: 11 additions & 0 deletions zipline/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,17 @@ class WindowLengthNotSpecified(ZiplineError):
)


class InvalidTermParams(ZiplineError):
"""
Raised if a user attempts to construct a Term using ParameterizedTermMixin
without specifying a `params` list in the class body.
"""
msg = (
"Expected a list of strings as a class-level attribute for "
"{termname}.params, but got {value} instead."
)


class DTypeNotSpecified(ZiplineError):
"""
Raised if a user attempts to construct a term without specifying dtype and
Expand Down
12 changes: 11 additions & 1 deletion zipline/pipeline/factors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
BusinessDaysUntilNextEarnings,
)
from .technical import (
DollarVolume,
EWMA,
EWMSTD,
ExponentialWeightedMovingAverage,
ExponentialWeightedStandardDeviation,
MaxDrawdown,
RSI,
Returns,
Expand All @@ -17,9 +22,14 @@
)

__all__ = [
'CustomFactor',
'BusinessDaysSincePreviousEarnings',
'BusinessDaysUntilNextEarnings',
'CustomFactor',
'DollarVolume',
'EWMA',
'EWMSTD',
'ExponentialWeightedMovingAverage',
'ExponentialWeightedStandardDeviation',
'Factor',
'Latest',
'MaxDrawdown',
Expand Down
Loading