diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 224417d5..488367ac 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -116,8 +116,12 @@ class TestMiscellaneousAPI(TestCase): setup_logger(self) sids = [1, 2] - self.sim_params = factory.create_simulation_parameters(num_days=2, - sids=sids) + self.sim_params = factory.create_simulation_parameters( + num_days=2, + sids=sids, + data_frequency='minute', + emission_rate='minute', + ) self.source = factory.create_minutely_trade_source( sids, trade_count=100, diff --git a/tests/utils/test_events.py b/tests/utils/test_events.py index 92c5017b..2792ce49 100644 --- a/tests/utils/test_events.py +++ b/tests/utils/test_events.py @@ -47,10 +47,10 @@ from zipline.utils.events import ( # A day known to be a half day. -HALF_DAY = datetime.date(year=2014, month=7, day=3) +HALF_DAY = datetime.datetime(year=2014, month=7, day=3) # A day known to be a full day. -FULL_DAY = datetime.date(year=2014, month=9, day=24) +FULL_DAY = datetime.datetime(year=2014, month=9, day=24) def param_range(*args): @@ -66,21 +66,25 @@ class TestUtils(TestCase): with self.assertRaises(ValueError): f(None, {}) + def test_build_offset_default(self): + default = object() + self.assertIs(default, _build_offset(None, {}, default)) + def test_build_offset_both(self): with self.assertRaises(ValueError): - _build_offset(datetime.timedelta(minutes=1), {'minutes': 1}) + _build_offset(datetime.timedelta(minutes=1), {'minutes': 1}, None) def test_build_offset_kwargs(self): kwargs = {'minutes': 1} self.assertEqual( - _build_offset(None, kwargs), + _build_offset(None, kwargs, None), datetime.timedelta(**kwargs), ) def test_build_offset_td(self): td = datetime.timedelta(minutes=1) self.assertEqual( - _build_offset(td, {}), + _build_offset(td, {}, None), td, ) @@ -155,7 +159,7 @@ class TestEventManager(TestCase): for r in [CountingRule] * 5: self.em.add_event( - Event(r(), lambda context, data: None, check_args=False) + Event(r(), lambda context, data: None) ) self.em.handle_data(None, None, datetime.datetime.now()) @@ -270,18 +274,18 @@ class TestStatelessRules(RuleTestCase): @parameterized.expand(param_range(5)) def test_NthTradingDayOfWeek(self, n): should_trigger = NthTradingDayOfWeek(n).should_trigger - prev_day = None + prev_day = self.sept_week[0].date() n_tdays = 0 for m in dropwhile(lambda n: not should_trigger(n), self.sept_week): + if prev_day < m.date(): + n_tdays += 1 + prev_day = m.date() + if should_trigger(m): self.assertEqual(n_tdays, n) else: self.assertNotEqual(n_tdays, n) - if not prev_day or prev_day < m.date(): - n_tdays += 1 - prev_day = m.date() - @parameterized.expand(param_range(5)) def test_NDaysBeforeLastTradingDayOfWeek(self, n): should_trigger = NDaysBeforeLastTradingDayOfWeek(n).should_trigger @@ -290,7 +294,7 @@ class TestStatelessRules(RuleTestCase): n_tdays = 0 date = m.to_datetime().date() next_date = self.env.next_trading_day(date) - while next_date.day > date.day: + while next_date.weekday() > date.weekday(): date = next_date next_date = self.env.next_trading_day(date) n_tdays += 1 diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 57de4c91..16431413 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -34,6 +34,7 @@ from zipline.errors import ( UnsupportedCommissionModel, UnsupportedOrderParameters, UnsupportedSlippageModel, + IncompatibleScheduleFunctionDataFrequency, ) from zipline.finance import trading @@ -218,7 +219,6 @@ class TradingAlgorithm(object): # We pass handle_data.__func__ to get the unbound method. # We will explicitly pass the algorithm to bind it again. self.handle_data.__func__, - check_args=False, ), prepend=True, ) @@ -512,12 +512,12 @@ class TradingAlgorithm(object): def get_environment(self): return self._environment - def add_event(self, rule=None, callback=None, check_args=True): + def add_event(self, rule=None, callback=None): """ Adds an event to the algorithm's EventManager. """ self.event_manager.add_event( - zipline.utils.events.Event(rule, callback, check_args=check_args), + zipline.utils.events.Event(rule, callback), ) @api_method @@ -525,11 +525,13 @@ class TradingAlgorithm(object): func, date_rule=None, time_rule=None, - half_days=True, - check_args=False): + half_days=True): """ Schedules a function to be called with some timed rules. """ + if self.sim_params.data_frequency != 'minute': + raise IncompatibleScheduleFunctionDataFrequency() + # Defaults to every day 30 minutes before close. date_rule = date_rule or DateRuleFactory.every_day() time_rule = time_rule or TimeRuleFactory.market_close(minutes=30) @@ -537,7 +539,6 @@ class TradingAlgorithm(object): self.add_event( make_eventrule(date_rule, time_rule, half_days), func, - check_args=check_args, ) @api_method diff --git a/zipline/errors.py b/zipline/errors.py index 6843de60..8a979faa 100644 --- a/zipline/errors.py +++ b/zipline/errors.py @@ -170,3 +170,12 @@ class IncompatibleHistoryFrequency(ZiplineError): Requested history at frequency '{frequency}' cannot be created with data at frequency '{data_frequency}'. """.strip() + + +class IncompatibleScheduleFunctionDataFrequency(ZiplineError): + """ + Raised when schedule function is used in daily mode. + """ + msg = """ + schedule_function may only be used in minute mode. + """ diff --git a/zipline/utils/events.py b/zipline/utils/events.py index 0247e4b8..a7e86978 100644 --- a/zipline/utils/events.py +++ b/zipline/utils/events.py @@ -21,7 +21,6 @@ import pandas as pd import pytz from zipline.finance.trading import TradingEnvironment -from zipline.utils.argcheck import verify_callable_argspec, Argument __all__ = [ @@ -67,13 +66,13 @@ def ensure_utc(time, tz='UTC'): return time.replace(tzinfo=pytz.utc) -def _build_offset(offset, kwargs): +def _build_offset(offset, kwargs, default): """ Builds the offset argument for event rules. """ if offset is None: if not kwargs: - return datetime.timedelta() # An empty offset (+0). + return default # use the default. else: return datetime.timedelta(**kwargs) elif kwargs: @@ -143,24 +142,8 @@ class Event(namedtuple('Event', ['rule', 'callback'])): with the current algorithm context, data, and datetime only when the rule is triggered. """ - def __new__(cls, rule=None, callback=None, check_args=True): + def __new__(cls, rule=None, callback=None): callback = callback or (lambda *args, **kwargs: None) - if check_args: - # Check the callback provided. - verify_callable_argspec( - callback, - [Argument('context' if check_args else Argument.ignore), - Argument('data' if check_args else Argument.ignore)] - ) - - # Make sure that the rule's should_trigger is valid. This will - # catch potential errors much more quickly and give a more helpful - # error. - verify_callable_argspec( - getattr(rule, 'should_trigger'), - [Argument('dt')] - ) - return super(cls, cls).__new__(cls, rule=rule, callback=callback) def handle_data(self, context, data, dt): @@ -279,7 +262,11 @@ class AfterOpen(StatelessRule): >>> AfterOpen(minutes=30) """ def __init__(self, offset=None, **kwargs): - self.offset = _build_offset(offset, kwargs) + self.offset = _build_offset( + offset, + kwargs, + datetime.timedelta(), # Defaults to the first minute. + ) def should_trigger(self, dt): return self.env.get_open_and_close(dt)[0] + self.offset <= dt @@ -293,7 +280,11 @@ class BeforeClose(StatelessRule): >>> BeforeClose(minutes=30) """ def __init__(self, offset=None, **kwargs): - self.offset = _build_offset(offset, kwargs) + self.offset = _build_offset( + offset, + kwargs, + datetime.timedelta(minutes=1), # Defaults to the last minute. + ) def should_trigger(self, dt): return self.env.get_open_and_close(dt)[1] - self.offset < dt @@ -304,7 +295,7 @@ class NotHalfDay(StatelessRule): A rule that only triggers when it is not a half day. """ def should_trigger(self, dt): - return dt not in self.env.early_closes + return dt.date() not in self.env.early_closes class NthTradingDayOfWeek(StatelessRule): @@ -326,9 +317,7 @@ class NthTradingDayOfWeek(StatelessRule): def get_first_trading_day_of_week(self, dt): prev = dt dt = self.env.previous_trading_day(dt) - # Backtrack until we hit a week border, then jump to the next trading - # day. - while dt.day < prev.day: + while dt.date().weekday() < prev.date().weekday(): prev = dt dt = self.env.previous_trading_day(dt) return prev.date() @@ -355,7 +344,7 @@ class NDaysBeforeLastTradingDayOfWeek(StatelessRule): dt = self.env.next_trading_day(dt) # Traverse forward until we hit a week border, then jump back to the # previous trading day. - while dt.day > prev.day: + while dt.date().weekday() > prev.date().weekday(): prev = dt dt = self.env.next_trading_day(dt) return prev.date() @@ -482,20 +471,20 @@ class DateRuleFactory(object): every_day = Always @staticmethod - def month_start(offset=0): - return NthTradingDayOfMonth(n=offset) + def month_start(days_offset=0): + return NthTradingDayOfMonth(n=days_offset) @staticmethod - def month_end(offset=0): - return NDaysBeforeLastTradingDayOfMonth(n=offset) + def month_end(days_offset=0): + return NDaysBeforeLastTradingDayOfMonth(n=days_offset) @staticmethod - def week_start(offset=0): - return NthTradingDayOfWeek(n=offset) + def week_start(days_offset=0): + return NthTradingDayOfWeek(n=days_offset) @staticmethod - def week_end(offset=0): - return NDaysBeforeLastTradingDayOfWeek(n=offset) + def week_end(days_offset=0): + return NDaysBeforeLastTradingDayOfWeek(n=days_offset) class TimeRuleFactory(object):