diff --git a/tests/test_exchange_calendar.py b/tests/test_exchange_calendar.py index d91ba3b8..a80977fd 100644 --- a/tests/test_exchange_calendar.py +++ b/tests/test_exchange_calendar.py @@ -112,7 +112,7 @@ class ExchangeCalendarTestBase(object): def test_minute_window(self): for open in self.answers.market_open: open_tz = open.tz_localize('UTC') - window = self.calendar.minute_window(open_tz, 390, 1) + window = self.calendar.market_minute_window(open_tz, 390, step=1) self.assertEqual(len(window), 390) diff --git a/tests/test_finance.py b/tests/test_finance.py index ea5ac0bb..df1cf72d 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -203,7 +203,7 @@ class FinanceTestCase(WithLogger, data_frequency="minute" ) - minutes = default_nyse_schedule.minute_window( + minutes = default_nyse_schedule.execution_minute_window( sim_params.first_open, int((trade_interval.total_seconds() / 60) * trade_count) + 100) @@ -497,7 +497,7 @@ class TradingEnvironmentTestCase(WithLogger, utc_start = pd.Timestamp(start.astimezone(utc)) # Get the next 10 minutes - minutes = self.cal.minute_window( + minutes = self.cal.market_minute_window( utc_start, 10, ) self.assertEqual(len(minutes), 10) @@ -505,7 +505,7 @@ class TradingEnvironmentTestCase(WithLogger, self.assertEqual(minutes[i], utc_start + timedelta(minutes=i)) # Get the previous 10 minutes. - minutes = self.cal.minute_window( + minutes = self.cal.market_minute_window( utc_start, 10, step=-1, ) self.assertEqual(len(minutes), 10) @@ -518,14 +518,14 @@ class TradingEnvironmentTestCase(WithLogger, # Today: 10:01 AM -> 4:00 PM (360 minutes) # Tomorrow: 9:31 AM -> 4:00 PM (390 minutes, 750 total) # Last Day: 9:31 AM -> 12:00 PM (150 minutes, 900 total) - minutes = self.cal.minute_window( + minutes = self.cal.market_minute_window( start, 900, ) - today = self.cal.minutes_for_date(utc_start)[30:] - tomorrow = self.cal.minutes_for_date( + today = self.cal.trading_minutes_for_day(utc_start)[30:] + tomorrow = self.cal.trading_minutes_for_day( start + timedelta(days=1) ) - last_day = self.cal.minutes_for_date( + last_day = self.cal.trading_minutes_for_day( start + timedelta(days=2))[:150] self.assertEqual(len(minutes), 900) @@ -540,17 +540,17 @@ class TradingEnvironmentTestCase(WithLogger, # Today: 10:01 AM -> 9:31 AM (31 minutes) # Friday: 4:00 PM -> 9:31 AM (390 minutes, 421 total) # Thursday: 4:00 PM -> 9:41 AM (380 minutes, 801 total) - minutes = self.cal.minute_window( + minutes = self.cal.market_minute_window( start, 801, step=-1, ) - today = self.cal.minutes_for_date(utc_start)[30::-1] + today = self.cal.trading_minutes_for_day(utc_start)[30::-1] # minus an extra two days from each of these to account for the two # weekend days we skipped - friday = self.cal.minutes_for_date( + friday = self.cal.trading_minutes_for_day( start + timedelta(days=-3), )[::-1] - thursday = self.cal.minutes_for_date( + thursday = self.cal.trading_minutes_for_day( start + timedelta(days=-4), )[:9:-1] diff --git a/zipline/utils/calendars/calendar_helpers.py b/zipline/utils/calendars/calendar_helpers.py index f78e6cf6..c660bf42 100644 --- a/zipline/utils/calendars/calendar_helpers.py +++ b/zipline/utils/calendars/calendar_helpers.py @@ -16,6 +16,7 @@ import pandas as pd import numpy as np import bisect +from pytz import timezone from zipline.errors import NoFurtherDataError @@ -110,11 +111,13 @@ def days_in_range(start, end, all_days): Get all execution days between start and end, inclusive. """ + start_date = normalize_date(start) end_date = normalize_date(end) + return all_days[all_days.slice_indexer(start_date, end_date)] - mask = ((all_days >= start_date) & (all_days <= end_date)) - return all_days[mask] + #mask = ((all_days >= start_date) & (all_days <= end_date)) + #return all_days[mask] def minutes_for_days_in_range(start, end, days_in_range_hook, @@ -211,3 +214,60 @@ def previous_scheduled_minute(start, is_scheduled_day_hook, # If start is not a trading day, or is before the market open # then return the close of the *previous* trading day. return previous_open_and_close_hook(start)[1] + + +def minute_window(start, count, schedule, is_scheduled_minute_hook, + session_date_hook, minutes_for_date_hook, step=1): + """ + Returns a DatetimeIndex containing `count` market minutes, starting + with `start` and continuing `step` minutes at a time. + + Parameters + ---------- + start : Timestamp + The start of the window. + count : int + The number of minutes needed. + step : int + The step size by which to increment. + + Returns + ------- + DatetimeIndex + A window with @count minutes, start with @start. + """ + if not is_scheduled_minute_hook(start): + raise ValueError("minute_window starting at non-market time " + "{minute}".format(minute=start)) + + start_utc = start.astimezone(timezone('UTC')) + + session = session_date_hook(start) + session_idx = schedule.index.get_loc(session) + + mins_in_session = minutes_for_date_hook(session) + start_idx = mins_in_session.searchsorted(start_utc) + + # Use a list instead of a pandas DatetimeIndex, as using .append() + # with DatetimeIndex can become expensive if used several times, since + # it makes a full copy of the data. list.extend() will not typically + # copy the data unless there is not enough memory to extend into, which + # is usually not problem. + all_minutes = list(mins_in_session[start_idx::np.sign(step)]) + + while True: + + step_minutes = all_minutes[0::np.absolute(step)] + + if len(step_minutes) >= count: + step_minutes = step_minutes[:count] + return pd.DatetimeIndex(step_minutes, copy=False) + + # Iterate session forward or backward + session_idx += np.sign(step) + # Get the minutes in the next exchange session + session = schedule.index[session_idx] + session_minutes = minutes_for_date_hook(session)[::np.sign(step)] + + # A these new session_minutes to the `all_minutes` candidate list + all_minutes.extend(list(session_minutes)) diff --git a/zipline/utils/cme_exchange_calendar.py b/zipline/utils/calendars/cme_exchange_calendar.py similarity index 80% rename from zipline/utils/cme_exchange_calendar.py rename to zipline/utils/calendars/cme_exchange_calendar.py index 9aeb9ad2..fd67511d 100644 --- a/zipline/utils/cme_exchange_calendar.py +++ b/zipline/utils/calendars/cme_exchange_calendar.py @@ -374,81 +374,3 @@ class CMEExchangeCalendar(ExchangeCalendar): """ dt_utc = dt.tz_convert('UTC').tz_convert(None) return dt_utc.replace(hour=0, minute=0, second=0) - - def minutes_for_date(self, date): - """ - Given a UTC-canonicalized date, returns a DatetimeIndex of all trading - minutes in the exchange session for that date. - - SD: Sounds like @date can be an arbitrary datetime, and that we should - first map to an exchange session by calling self.session_date. Need to - check what the consumers expect. - - Parameters - ---------- - date : Timestamp - The UTC-canonicalized date whose minutes are needed. - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the minutes in the - given date. - """ - open, close = self.open_and_close(date) - return date_range(open, close, freq='min', tz='UTC') - - def minute_window(self, start, count, step=1): - """ - Return a DatetimeIndex containing `count` market minutes, starting with - `start` and continuing `step` minutes at a time. - - Parameters - ---------- - start : Timestamp - The start of the window. - count : int - The number of minutes needed. - step : int - The step size by which to increment. - - Returns - ------- - DatetimeIndex - A window with @count minutes, start with @start. - """ - if not self.is_open(start): - raise ValueError("minute_window starting at non-market time " - "{minute}".format(minute=start)) - - start_utc = start.tz_convert('UTC') - - session = self.session_date(start) - session_idx = self.schedule.index.get_loc(session) - - mins_in_session = self.minutes_for_date(session) - start_idx = mins_in_session.searchsorted(start_utc) - - # Use a list instead of a pandas DatetimeIndex, as using .append() - # with DatetimeIndex can become expensive if used several times, since - # it makes a full copy of the data. list.extend() will not typically - # copy the data unless there is not enough memory to extend into, which - # is usually not problem. - all_minutes = list(mins_in_session[start_idx::np.sign(step)]) - - while True: - - step_minutes = all_minutes[0::step] - - if len(step_minutes) >= count: - step_minutes = step_minutes[:count] - return pd.DatetimeIndex(step_minutes, copy=False) - - # Iterate session forward or backward - session_idx += np.sign(step) - # Get the minutes in the next exchange session - session = self.schedule.index[session_idx] - session_minutes = self.minutes_for_date(session) - - # A these new session_minutes to the `all_minutes` candidate list - all_minutes.extend(list(session_minutes)) diff --git a/zipline/utils/calendars/exchange_calendar.py b/zipline/utils/calendars/exchange_calendar.py index d1f28d9f..012bdf30 100644 --- a/zipline/utils/calendars/exchange_calendar.py +++ b/zipline/utils/calendars/exchange_calendar.py @@ -48,6 +48,7 @@ from .calendar_helpers import ( all_scheduled_minutes, next_scheduled_minute, previous_scheduled_minute, + minute_window, ) start_default = pd.Timestamp('1990-01-01', tz='UTC') @@ -242,6 +243,13 @@ class ExchangeCalendar(with_metaclass(ABCMeta)): open_and_close_hook=self.open_and_close, previous_open_and_close_hook=self.previous_open_and_close, ) + self.market_minute_window = partial( + minute_window, + schedule=self.schedule, + is_scheduled_minute_hook=self.is_open_on_minute, + session_date_hook=self.session_date, + minutes_for_date_hook=self.trading_minutes_for_day, + ) def _special_dates(self, calendars, ad_hoc_dates, start_date, end_date): """ @@ -413,56 +421,10 @@ class ExchangeCalendar(with_metaclass(ABCMeta)): """ raise NotImplementedError() - @abstractmethod - def minutes_for_date(self, date): - """ - Given a UTC-canonicalized date, returns a DatetimeIndex of all trading - minutes in the exchange session for that date. - - SD: Sounds like @date can be an arbitrary datetime, and that we should - first map to an exchange session by calling self.session_date. Need to - check what the consumers expect. - - Parameters - ---------- - date : Timestamp - The UTC-canonicalized date whose minutes are needed. - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the minutes in the - given date. - """ - raise NotImplementedError() - - @abstractmethod - def minute_window(self, start, count, step=1): - """ - Return a DatetimeIndex containing `count` market minutes, starting with - `start` and continuing `step` minutes at a time. - - Parameters - ---------- - start : Timestamp - The start of the window. - count : int - The number of minutes needed. - step : int - The step size by which to increment. - - Returns - ------- - DatetimeIndex - A window with @count minutes, starting with @start a returning - every @step minute. - """ - raise NotImplementedError() - _static_calendars = {} -_lazy_calendar_names = ['NYSE'] +_lazy_calendar_names = ['NYSE', 'CME'] def get_calendar(name): @@ -481,12 +443,18 @@ def get_calendar(name): # It's not a lazy calendar, so raise an exception raise InvalidCalendarName(calendar_name=name) - if name is 'NYSE': + if name == 'NYSE': from zipline.utils.calendars.nyse_exchange_calendar \ import NYSEExchangeCalendar nyse_cal = NYSEExchangeCalendar() register_calendar(nyse_cal) + if name == 'CME': + from zipline.utils.calendars.cme_exchange_calendar \ + import CMEExchangeCalendar + cme_cal = CMEExchangeCalendar() + register_calendar(cme_cal) + return _static_calendars[name] diff --git a/zipline/utils/calendars/nyse_exchange_calendar.py b/zipline/utils/calendars/nyse_exchange_calendar.py index 59f08ece..0d0ef966 100644 --- a/zipline/utils/calendars/nyse_exchange_calendar.py +++ b/zipline/utils/calendars/nyse_exchange_calendar.py @@ -16,8 +16,6 @@ from datetime import time from itertools import chain -import numpy as np -import pandas as pd from dateutil.relativedelta import ( MO, TH, @@ -440,83 +438,3 @@ class NYSEExchangeCalendar(ExchangeCalendar): while not self.is_open_on_day(dt): dt += Timedelta(days=1) return normalize_date(dt) - - def minutes_for_date(self, dt): - """ - Given a datetime, returns a DatetimeIndex of all trading - minutes in the exchange session for that datetime. - - SD: Should @dt be an arbitrary datetime, so that we should - first map to an exchange session by calling self.session_date. Need to - check what the consumers expect. Here, I assume we need to map it to a - session. - - Parameters - ---------- - dt : Timestamp - The datetime whose exchange session minutes are needed. - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the minutes in the - given dt. - """ - session = self.session_date(dt) - open, close = self.open_and_close(session) - return date_range(open, close, freq='min', tz='UTC') - - def minute_window(self, start, count, step=1): - """ - Returns a DatetimeIndex containing `count` market minutes, starting - with `start` and continuing `step` minutes at a time. - - Parameters - ---------- - start : Timestamp - The start of the window. - count : int - The number of minutes needed. - step : int - The step size by which to increment. - - Returns - ------- - DatetimeIndex - A window with @count minutes, start with @start. - """ - if not self.is_open_on_minute(start): - raise ValueError("minute_window starting at non-market time " - "{minute}".format(minute=start)) - - start_utc = start.astimezone(timezone('UTC')) - - session = self.session_date(start) - session_idx = self.schedule.index.get_loc(session) - - mins_in_session = self.minutes_for_date(session) - start_idx = mins_in_session.searchsorted(start_utc) - - # Use a list instead of a pandas DatetimeIndex, as using .append() - # with DatetimeIndex can become expensive if used several times, since - # it makes a full copy of the data. list.extend() will not typically - # copy the data unless there is not enough memory to extend into, which - # is usually not problem. - all_minutes = list(mins_in_session[start_idx::np.sign(step)]) - - while True: - - step_minutes = all_minutes[0::np.absolute(step)] - - if len(step_minutes) >= count: - step_minutes = step_minutes[:count] - return pd.DatetimeIndex(step_minutes, copy=False) - - # Iterate session forward or backward - session_idx += np.sign(step) - # Get the minutes in the next exchange session - session = self.schedule.index[session_idx] - session_minutes = self.minutes_for_date(session)[::np.sign(step)] - - # A these new session_minutes to the `all_minutes` candidate list - all_minutes.extend(list(session_minutes)) diff --git a/zipline/utils/calendars/trading_schedule.py b/zipline/utils/calendars/trading_schedule.py index d82e691f..eeef9ea9 100644 --- a/zipline/utils/calendars/trading_schedule.py +++ b/zipline/utils/calendars/trading_schedule.py @@ -19,6 +19,7 @@ from abc import ( abstractproperty, ) from functools import partial +from six import with_metaclass from zipline.utils.memoize import remember_last @@ -36,14 +37,14 @@ from .calendar_helpers import ( all_scheduled_minutes, next_scheduled_minute, previous_scheduled_minute, + minute_window, ) -class TradingSchedule(object): +class TradingSchedule(with_metaclass(ABCMeta)): """ A TradingSchedule defines the execution timing of a TradingAlgorithm. """ - __metaclass__ = ABCMeta def __init__(self): # Assign the partial calendar helpers @@ -102,12 +103,19 @@ class TradingSchedule(object): open_and_close_hook=self.start_and_end, previous_open_and_close_hook=self.previous_start_and_end, ) + self.execution_minute_window = partial( + minute_window, + schedule=self.schedule, + is_scheduled_minute_hook=self.is_executing_on_minute, + session_date_hook=self.session_date, + minutes_for_date_hook=self.execution_minutes_for_day, + ) @abstractproperty def day(self): """ A CustomBusinessDay defining those days on which the algorithm is - usually trading. + trading. """ raise NotImplementedError() @@ -257,26 +265,23 @@ class TradingSchedule(object): """ raise NotImplementedError() + @abstractmethod - def minute_window(self, start, count, step=1): + def session_date(self, dt): """ - Return a DatetimeIndex containing `count` market minutes, starting with - `start` and continuing `step` minutes at a time. + Given a time, returns the UTC-canonicalized date of the trading + session in which the time belongs. If the time is not in a trading + session (while algorithm isn't trading), returns the date of the next + exchange session after the time. Parameters ---------- - start : Timestamp - The start of the window. - count : int - The number of minutes needed. - step : int - The step size by which to increment. + dt : Timestamp Returns ------- - DatetimeIndex - A window with @count minutes, starting with @start a returning - every @step minute. + Timestamp + The date of the exchange session in which dt belongs. """ raise NotImplementedError() @@ -358,10 +363,11 @@ class ExchangeTradingSchedule(TradingSchedule): """ return self._exchange_calendar.is_open_on_day(dt) - def minute_window(self, start, count, step=1): - return self._exchange_calendar.minute_window(start=start, - count=count, - step=step) + def session_date(self, dt): + """ + See TradingSchedule definition. + """ + return self._exchange_calendar.session_date(dt) @property def early_ends(self): diff --git a/zipline/utils/simfactory.py b/zipline/utils/simfactory.py index 4388f9d1..7ce81c23 100644 --- a/zipline/utils/simfactory.py +++ b/zipline/utils/simfactory.py @@ -38,21 +38,9 @@ def create_test_zipline(**config): "argument 'sid_list' or 'sid'") concurrent_trades = config.get('concurrent_trades', False) - - if 'order_count' in config: - order_count = config['order_count'] - else: - order_count = 100 - - if 'order_amount' in config: - order_amount = config['order_amount'] - else: - order_amount = 100 - - if 'trading_schedule' in config: - trading_schedule = config['trading_schedule'] - else: - trading_schedule = default_nyse_schedule + order_count = config.get('order_count', 100) + order_amount = config.get('order_amount', 100) + trading_schedule = config.get('trading_schedule', default_nyse_schedule) # ------------------- # Create the Algo