Skip to content

Commit

Permalink
ENH: Adds a complete set of EventRules
Browse files Browse the repository at this point in the history
  • Loading branch information
llllllllll committed Nov 3, 2014
1 parent dfb5e93 commit 9f275b5
Show file tree
Hide file tree
Showing 4 changed files with 476 additions and 61 deletions.
2 changes: 1 addition & 1 deletion tests/test_algorithm.py
Expand Up @@ -806,7 +806,7 @@ def _check_algo(self,
expected_order_count,
expected_exc):

algo._add_handle_data(handle_data)
algo._handle_data = handle_data
with self.assertRaises(expected_exc) if expected_exc else nullctx():
algo.run(self.source)
self.assertEqual(algo.order_count, expected_order_count)
Expand Down
155 changes: 145 additions & 10 deletions tests/utils/test_events.py
Expand Up @@ -14,31 +14,44 @@
# limitations under the License.
import datetime
import random
from itertools import islice, dropwhile
from itertools import islice, product
import operator
from six.moves import range, map
from nose_parameterized import parameterized
from unittest import TestCase

import pandas as pd
import numpy as np

from zipline.finance.trading import TradingEnvironment
import zipline.utils.events
from zipline.utils import events as events_module
from zipline.utils.events import (
EventRule,
StatelessRule,
Always,
Never,
InvertedRule,
AfterOpen,
ComposedRule,
BeforeClose,
OnDate,
BeforeDate,
AfterDate,
AtTime,
AfterTime,
BeforeTime,
HalfDay,
NotHalfDay,
NthTradingDayOfWeek,
NDaysBeforeLastTradingDayOfWeek,
NthTradingDayOfMonth,
NDaysBeforeLastTradingDayOfMonth,
StatefulRule,
DoNTimes,
SkipNTimes,
NTimesPerPeriod,
OncePerDay,
RuleFromCallable,
_build_offset,
_build_date,
_build_time,
Expand Down Expand Up @@ -82,10 +95,6 @@ def test_build_offset_exc(self):
# object() is not an instance of a timedelta.
_build_offset(object(), {}, None)

def test_build_offset_both(self):
with self.assertRaises(ValueError):
_build_offset(datetime.timedelta(minutes=1), {'minutes': 1})

def test_build_offset_kwargs(self):
kwargs = {'minutes': 1}
self.assertEqual(
Expand Down Expand Up @@ -262,6 +271,16 @@ def test_Never(self):
should_trigger = Never().should_trigger
self.assertFalse(any(map(should_trigger, self.minutes)))

def test_InvertedRule(self):
rule = Always()
should_trigger = rule.should_trigger
should_not_trigger = InvertedRule(rule).should_trigger
f = lambda m: should_trigger(m) != should_not_trigger(m)
self.assertTrue(all(map(f, self.minutes)))

# Test the syntax.
self.assertIsInstance(~Always(), InvertedRule)

def test_AfterOpen(self):
should_trigger = AfterOpen(minutes=5, hours=1).should_trigger
for d in self.trading_days:
Expand All @@ -278,6 +297,60 @@ def test_BeforeClose(self):
for m in d[-65:]:
self.assertTrue(should_trigger(m))

def test_OnDate(self):
first_day = next(self.trading_days)
should_trigger = OnDate(first_day[0].date()).should_trigger
self.assertTrue(all(map(should_trigger, first_day)))
self.assertFalse(any(map(should_trigger, self.minutes)))

def _test_before_after_date(self, class_, op):
minutes = list(self.minutes)
half = int(len(minutes) / 2)
should_trigger = class_(minutes[half].date()).should_trigger
for m in minutes:
if op(m.date(), minutes[half].date()):
self.assertTrue(should_trigger(m))
else:
self.assertFalse(should_trigger(m))

def test_BeforeDate(self):
self._test_before_after_date(BeforeDate, operator.lt)

def test_AfterDate(self):
self._test_before_after_date(AfterDate, operator.gt)

def test_AtTime(self):
time = datetime.time(hour=15, minute=5)
should_trigger = AtTime(time).should_trigger

hit = []
f = lambda m: should_trigger(m) == (m.time() == time) \
and (hit.append(None) or True)
self.assertTrue(all(map(f, self.minutes)))
# Make sure we actually had a bar that is the time we wanted.
self.assertTrue(hit)

def _test_before_after_time(self, class_, op):
time = datetime.time(hour=15, minute=5)
should_trigger = class_(time).should_trigger

for m in self.minutes:
if op(m.time(), time):
self.assertTrue(should_trigger(m))
else:
self.assertFalse(should_trigger(m))

def test_BeforeTime(self):
self._test_before_after_time(BeforeTime, operator.lt)

def test_AfterTime(self):
self._test_before_after_time(AfterTime, operator.gt)

def test_HalfDay(self):
should_trigger = HalfDay().should_trigger
self.assertTrue(should_trigger(HALF_DAY))
self.assertFalse(should_trigger(FULL_DAY))

def test_NotHalfDay(self):
should_trigger = NotHalfDay().should_trigger
self.assertTrue(should_trigger(FULL_DAY))
Expand All @@ -302,7 +375,7 @@ def test_NthTradingDayOfWeek(self, n):
n_tdays += 1
prev_day = m.date()

@parameterized.expand(param_range(5))
@parameterized.expand(param_range(MAX_WEEK_RANGE))
def test_NDaysBeforeLastTradingDayOfWeek(self, n):
should_trigger = NDaysBeforeLastTradingDayOfWeek(n).should_trigger
for m in self.sept_week:
Expand Down Expand Up @@ -338,15 +411,29 @@ def test_NDaysBeforeLastTradingDayOfMonth(self, n):
else:
self.assertNotEqual(n_days_before, n)

def test_ComposedRule(self):
@parameterized.expand([
('and', operator.and_, lambda t: t._test_composed_and),
('or', operator.or_, lambda t: t._test_composed_or),
('xor', operator.xor, lambda t: t._test_composed_xor),
])
def test_ComposedRule(self, name, composer, tester):
rule1 = Always()
rule2 = Never()

composed = rule1 & rule2
composed = composer(rule1, rule2)
self.assertIsInstance(composed, ComposedRule)
self.assertIs(composed.first, rule1)
self.assertIs(composed.second, rule2)
self.assertFalse(any(map(composed.should_trigger, self.minutes)))
tester(self)(composed)

def _test_composed_and(self, rule):
self.assertFalse(any(map(rule.should_trigger, self.minutes)))

def _test_composed_or(self, rule):
self.assertTrue(all(map(rule.should_trigger, self.minutes)))

def _test_composed_xor(self, rule):
self.assertTrue(all(map(rule.should_trigger, self.minutes)))


class TestStatefulRules(RuleTestCase):
Expand All @@ -356,6 +443,54 @@ def setUpClass(cls):

cls.class_ = StatefulRule

@parameterized.expand(param_range(MAX_WEEK_RANGE))
def test_DoNTimes(self, n):
rule = DoNTimes(n)
min_gen = self.minutes

for n in range(n):
self.assertTrue(rule.should_trigger(next(min_gen)))

self.assertFalse(any(map(rule.should_trigger, min_gen)))

@parameterized.expand(param_range(MAX_WEEK_RANGE))
def test_SkipNTimes(self, n):
rule = SkipNTimes(n)
min_gen = self.minutes

for n in range(n):
self.assertFalse(rule.should_trigger(next(min_gen)))

self.assertTrue(any(map(rule.should_trigger, min_gen)))

@parameterized.expand(
product(
range(MAX_WEEK_RANGE), [('B', 5), ('W', 10), ('M', 50), ('Q', 50)],
)
)
def test_NTimesPerPeriod(self, n, period_ndays):
period, ndays = period_ndays
self.trading_days = self._get_random_days(ndays)

rule = NTimesPerPeriod(n=n, freq=period)

minutes = list(self.minutes)
hit = pd.Series(
0,
pd.date_range(minutes[0].date(), minutes[-1].date(), freq=period),
)

for m in self.minutes:
if rule.should_trigger(m):
hit[m] += 1

for h in hit:
self.assertLessEqual(h, n)

def test_RuleFromCallable(self):
rule = RuleFromCallable(lambda dt: True)
self.assertTrue(all(map(rule.should_trigger, self.minutes)))

def test_OncePerDay(self):
class RuleCounter(StatefulRule):
"""
Expand Down
50 changes: 5 additions & 45 deletions zipline/algorithm.py
Expand Up @@ -66,6 +66,7 @@
from zipline.sources import DataFrameSource, DataPanelSource
from zipline.transforms.utils import StatefulTransform
from zipline.utils.api_support import ZiplineAPI, api_method

import zipline.utils.events
from zipline.utils.events import (
EventManager,
Expand Down Expand Up @@ -200,7 +201,7 @@ def __init__(self, *args, **kwargs):
if 'handle_data' not in self.namespace:
raise ValueError('You must define a handle_data function.')
else:
self._add_handle_data(self.namespace['handle_data'])
self._handle_data = self.namespace['handle_data']

self._before_trading_start = \
self.namespace.get('before_trading_start')
Expand All @@ -212,7 +213,7 @@ def __init__(self, *args, **kwargs):
raise ValueError('You can not set script and \
initialize/handle_data.')
self._initialize = kwargs.pop('initialize')
self._add_handle_data(kwargs.pop('handle_data'))
self._handle_data = kwargs.pop('handle_data')
self._before_trading_start = kwargs.pop('before_trading_start',
None)

Expand Down Expand Up @@ -243,19 +244,6 @@ def __init__(self, *args, **kwargs):
if self.AUTO_INITIALIZE:
self.initialized = True

def _add_handle_data(self, handle_data):
"""
Adds the handle_data event.
"""
self.event_manager.add_event(
events_module.Event(
events_module.Always(),
handle_data,
check_args=False,
),
prepend=True,
)

def initialize(self, *args, **kwargs):
"""
Call self._initialize with `self` made available to Zipline API
Expand All @@ -274,7 +262,7 @@ def handle_data(self, data):
if self.history_container:
self.history_container.update(data, self.datetime)

self.event_manager.handle_data(self, data, self.datetime)
self._handle_data(self, data)

def analyze(self, perf):
if self._analyze is None:
Expand Down Expand Up @@ -531,6 +519,7 @@ def add_transform(self, transform_class, tag, *args, **kwargs):
def get_environment(self):
return self._environment

@api_method
def add_event(self, rule=None, callback=None):
"""
Adds an event to the algorithm's EventManager.
Expand Down Expand Up @@ -559,35 +548,6 @@ def schedule_function(self,
func,
)

@api_method
def add_event(self, rule=None, callback=None, check_args=True):
"""
Adds an event to the algorithm's EventManager.
"""
self.event_manager.add_event(
events_module.Event(rule, callback, check_args=check_args),
)

@api_method
def schedule_function(self,
func,
date_rule=None,
time_rule=None,
half_days=True,
check_args=False):
"""
Schedules a function to be called with some timed rules.
"""
# Defaults to every day 30 minutes before close.
date_rule = date_rule or DateRuleFactory.day()
time_rule = time_rule or TimeRuleFactory.market_close(minutes=30)

self.add_event(
make_eventrule(date_rule, time_rule, half_days),
func,
check_args=check_args,
)

@api_method
def record(self, *args, **kwargs):
"""
Expand Down

0 comments on commit 9f275b5

Please sign in to comment.