From 6fb4923cc74d4efe815ccdfc3bb514c7ad6feb1a Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Mon, 20 Jun 2016 10:30:31 -0400 Subject: [PATCH] Re-implemented the Calendar API. Instead of having separate ExchangeCalendar and TradingSchedule objects, we now just have TradingCalendar. The TradingCalendar keeps track of each session (defined as a contiguous set of minutes between an open and a close). It's also responsible for handling the grouping logic of any given minute to its containing session, or the next/previous session if it's not a market minute for the given calendar. --- setup.py | 6 +- tests/data/bundles/test_core.py | 4 +- tests/data/bundles/test_yahoo.py | 2 +- tests/data/test_minute_bars.py | 37 +- tests/data/test_us_equity_pricing.py | 22 +- tests/finance/test_slippage.py | 8 +- tests/pipeline/base.py | 6 +- tests/pipeline/test_engine.py | 20 +- tests/pipeline/test_frameload.py | 21 +- tests/pipeline/test_pipeline_algo.py | 47 +- tests/resources/calendars/nyse.csv | 2 +- tests/resources/yahoo_samples/rebuild_samples | 2 +- tests/risk/test_risk_cumulative.py | 24 +- tests/risk/test_risk_period.py | 133 ++- tests/test_algorithm.py | 202 +++-- tests/test_api_shim.py | 34 +- tests/test_assets.py | 6 +- tests/test_bar_data.py | 47 +- tests/test_benchmark.py | 48 +- tests/test_blotter.py | 12 +- tests/test_commissions.py | 6 +- tests/test_data_portal.py | 6 +- tests/test_exchange_calendar.py | 341 -------- tests/test_fetcher.py | 4 +- tests/test_finance.py | 45 +- tests/test_history.py | 117 ++- tests/test_perf_tracking.py | 185 +++-- tests/test_security_list.py | 41 +- tests/test_tradesimulation.py | 15 +- tests/test_trading_calendar.py | 762 ++++++++++++++++++ tests/test_trading_schedule.py | 109 --- tests/utils/test_events.py | 157 ++-- zipline/_protocol.pyx | 2 +- zipline/algorithm.py | 96 +-- zipline/data/bundles/core.py | 2 +- zipline/data/data_portal.py | 73 +- zipline/data/loader.py | 2 +- zipline/data/minute_bars.py | 3 +- zipline/data/us_equity_loader.py | 8 +- zipline/data/us_equity_pricing.py | 15 +- zipline/errors.py | 2 +- .../finance/performance/position_tracker.py | 2 +- zipline/finance/performance/tracker.py | 89 +- zipline/finance/risk/cumulative.py | 36 +- zipline/finance/risk/period.py | 39 +- zipline/finance/risk/report.py | 44 +- zipline/finance/risk/risk.py | 29 +- zipline/finance/trading.py | 180 +++-- zipline/gens/tradesimulation.py | 4 +- .../pipeline/loaders/equity_pricing_loader.py | 4 +- zipline/sources/benchmark_source.py | 36 +- zipline/sources/test_source.py | 30 +- zipline/testing/core.py | 95 ++- zipline/testing/fixtures.py | 107 +-- zipline/utils/calendars/__init__.py | 17 +- zipline/utils/calendars/_calendar_helpers.pyx | 53 ++ zipline/utils/calendars/calendar_helpers.py | 239 ------ zipline/utils/calendars/calendar_utils.py | 96 +++ zipline/utils/calendars/exchange_calendar.py | 588 -------------- .../utils/calendars/exchange_calendar_bmf.py | 172 +--- .../utils/calendars/exchange_calendar_cme.py | 345 +------- .../utils/calendars/exchange_calendar_lse.py | 173 +--- .../utils/calendars/exchange_calendar_nyse.py | 320 +------- .../utils/calendars/exchange_calendar_tsx.py | 171 +--- zipline/utils/calendars/trading_calendar.py | 732 +++++++++++++++++ zipline/utils/calendars/trading_schedule.py | 416 ---------- zipline/utils/calendars/us_holidays.py | 147 ++++ zipline/utils/events.py | 177 +--- zipline/utils/factory.py | 71 +- zipline/utils/run_algo.py | 4 +- zipline/utils/simfactory.py | 10 +- 71 files changed, 3119 insertions(+), 3981 deletions(-) delete mode 100644 tests/test_exchange_calendar.py create mode 100644 tests/test_trading_calendar.py delete mode 100644 tests/test_trading_schedule.py create mode 100644 zipline/utils/calendars/_calendar_helpers.pyx delete mode 100644 zipline/utils/calendars/calendar_helpers.py create mode 100644 zipline/utils/calendars/calendar_utils.py delete mode 100644 zipline/utils/calendars/exchange_calendar.py create mode 100644 zipline/utils/calendars/trading_calendar.py delete mode 100644 zipline/utils/calendars/trading_schedule.py create mode 100644 zipline/utils/calendars/us_holidays.py diff --git a/setup.py b/setup.py index ee44d023..776c8c69 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,11 @@ ext_modules = [ Extension( 'zipline.data._minute_bar_internal', ['zipline/data/_minute_bar_internal.pyx'] - ) + ), + Extension( + 'zipline.utils.calendars._calendar_helpers', + ['zipline/utils/calendars/_calendar_helpers.pyx'] + ), ] diff --git a/tests/data/bundles/test_core.py b/tests/data/bundles/test_core.py index 55ee4941..1a0b2484 100644 --- a/tests/data/bundles/test_core.py +++ b/tests/data/bundles/test_core.py @@ -111,9 +111,9 @@ class BundleCoreTestCase(WithInstanceTmpDir, ZiplineTestCase): def test_ingest(self): start = pd.Timestamp('2014-01-06', tz='utc') end = pd.Timestamp('2014-01-10', tz='utc') - trading_days = get_calendar('NYSE').all_trading_days + trading_days = get_calendar('NYSE').all_sessions calendar = trading_days[trading_days.slice_indexer(start, end)] - minutes = get_calendar('NYSE').trading_minutes_for_days_in_range( + minutes = get_calendar('NYSE').minutes_for_sessions_in_range( calendar[0], calendar[-1] ) diff --git a/tests/data/bundles/test_yahoo.py b/tests/data/bundles/test_yahoo.py index 5404da8e..8c41d9d3 100644 --- a/tests/data/bundles/test_yahoo.py +++ b/tests/data/bundles/test_yahoo.py @@ -18,7 +18,7 @@ class YahooBundleTestCase(WithResponses, ZiplineTestCase): columns = 'open', 'high', 'low', 'close', 'volume' asset_start = pd.Timestamp('2014-01-02', tz='utc') asset_end = pd.Timestamp('2014-12-31', tz='utc') - trading_days = get_calendar('NYSE').all_trading_days + trading_days = get_calendar('NYSE').all_sessions calendar = trading_days[ (trading_days >= asset_start) & (trading_days <= asset_end) diff --git a/tests/data/test_minute_bars.py b/tests/data/test_minute_bars.py index d0abcbde..a1253b29 100644 --- a/tests/data/test_minute_bars.py +++ b/tests/data/test_minute_bars.py @@ -45,7 +45,7 @@ from zipline.data.minute_bars import ( from zipline.testing.fixtures import ( WithInstanceTmpDir, - WithTradingSchedule, + WithTradingCalendar, ZiplineTestCase, ) @@ -56,17 +56,20 @@ TEST_CALENDAR_START = Timestamp('2014-06-02', tz='UTC') TEST_CALENDAR_STOP = Timestamp('2015-12-31', tz='UTC') -class BcolzMinuteBarTestCase(WithTradingSchedule, WithInstanceTmpDir, +class BcolzMinuteBarTestCase(WithTradingCalendar, WithInstanceTmpDir, ZiplineTestCase): @classmethod def init_class_fixtures(cls): super(BcolzMinuteBarTestCase, cls).init_class_fixtures() - trading_days = cls.trading_schedule.trading_sessions( - TEST_CALENDAR_START, TEST_CALENDAR_STOP - ) - cls.market_opens = trading_days.market_open - cls.market_closes = trading_days.market_close + + cal = cls.trading_calendar.schedule.loc[ + TEST_CALENDAR_START:TEST_CALENDAR_STOP + ] + + cls.market_opens = cal.market_open + cls.market_closes = cal.market_close + cls.test_calendar_start = cls.market_opens.index[0] cls.test_calendar_stop = cls.market_opens.index[-1] @@ -798,9 +801,9 @@ class BcolzMinuteBarTestCase(WithTradingSchedule, WithInstanceTmpDir, data = {sids[0]: data_1, sids[1]: data_2} start_minute_loc = \ - self.trading_schedule.all_execution_minutes.get_loc(minutes[0]) + self.trading_calendar.all_minutes.get_loc(minutes[0]) minute_locs = [ - self.trading_schedule.all_execution_minutes.get_loc(minute) + self.trading_calendar.all_minutes.get_loc(minute) - start_minute_loc for minute in minutes ] @@ -822,9 +825,11 @@ class BcolzMinuteBarTestCase(WithTradingSchedule, WithInstanceTmpDir, 'close': arange(1, 781), 'volume': arange(1, 781) } - dts = array(self.trading_schedule.execution_minutes_for_days_in_range( - start_day, end_day + dts = array(self.trading_calendar.minutes_for_sessions_in_range( + self.trading_calendar.minute_to_session_label(start_day), + self.trading_calendar.minute_to_session_label(end_day) )) + self.writer.write_cols(sid, dts, cols) self.assertEqual( @@ -866,9 +871,13 @@ class BcolzMinuteBarTestCase(WithTradingSchedule, WithInstanceTmpDir, 'close': arange(1, 601), 'volume': arange(1, 601) } - dts = array(self.trading_schedule.execution_minutes_for_days_in_range( - start_day, end_day - )) + dts = array( + self.trading_calendar.minutes_for_sessions_in_range( + self.trading_calendar.minute_to_session_label(start_day), + self.trading_calendar.minute_to_session_label(end_day) + ) + ) + self.writer.write_cols(sid, dts, cols) self.assertEqual( diff --git a/tests/data/test_us_equity_pricing.py b/tests/data/test_us_equity_pricing.py index b49dd0e2..cbbc428c 100644 --- a/tests/data/test_us_equity_pricing.py +++ b/tests/data/test_us_equity_pricing.py @@ -46,7 +46,6 @@ from zipline.testing.fixtures import ( WithBcolzEquityDailyBarReader, ZiplineTestCase, ) -from zipline.utils.calendars import get_calendar TEST_CALENDAR_START = Timestamp('2015-06-01', tz='UTC') TEST_CALENDAR_STOP = Timestamp('2015-06-30', tz='UTC') @@ -97,16 +96,17 @@ class BcolzDailyBarTestCase(WithBcolzEquityDailyBarReader, ZiplineTestCase): @classmethod def init_class_fixtures(cls): super(BcolzDailyBarTestCase, cls).init_class_fixtures() - cls.trading_days = get_calendar('NYSE').trading_days( - TEST_CALENDAR_START, TEST_CALENDAR_STOP - ).index + cls.sessions = cls.trading_calendar.sessions_in_range( + cls.trading_calendar.minute_to_session_label(TEST_CALENDAR_START), + cls.trading_calendar.minute_to_session_label(TEST_CALENDAR_STOP) + ) @property def assets(self): return EQUITY_INFO.index def trading_days_between(self, start, end): - return self.trading_days[self.trading_days.slice_indexer(start, end)] + return self.sessions[self.sessions.slice_indexer(start, end)] def asset_start(self, asset_id): return asset_start(EQUITY_INFO, asset_id) @@ -181,14 +181,14 @@ class BcolzDailyBarTestCase(WithBcolzEquityDailyBarReader, ZiplineTestCase): expected_calendar_offset, ) assert_index_equal( - self.trading_days, + self.sessions, DatetimeIndex(result.attrs['calendar'], tz='UTC'), ) def test_read_first_trading_day(self): self.assertEqual( self.bcolz_equity_daily_bar_reader.first_trading_day, - self.trading_days[0], + self.sessions[0], ) def _check_read_results(self, columns, assets, start_date, end_date): @@ -234,7 +234,7 @@ class BcolzDailyBarTestCase(WithBcolzEquityDailyBarReader, ZiplineTestCase): columns, self.assets, start_date=self.asset_start(asset), - end_date=self.trading_days[-1], + end_date=self.sessions[-1], ) def test_start_on_asset_end(self): @@ -248,7 +248,7 @@ class BcolzDailyBarTestCase(WithBcolzEquityDailyBarReader, ZiplineTestCase): columns, self.assets, start_date=self.asset_end(asset), - end_date=self.trading_days[-1], + end_date=self.sessions[-1], ) def test_end_on_asset_start(self): @@ -261,7 +261,7 @@ class BcolzDailyBarTestCase(WithBcolzEquityDailyBarReader, ZiplineTestCase): self._check_read_results( columns, self.assets, - start_date=self.trading_days[0], + start_date=self.sessions[0], end_date=self.asset_start(asset), ) @@ -275,7 +275,7 @@ class BcolzDailyBarTestCase(WithBcolzEquityDailyBarReader, ZiplineTestCase): self._check_read_results( columns, self.assets, - start_date=self.trading_days[0], + start_date=self.sessions[0], end_date=self.asset_end(asset), ) diff --git a/tests/finance/test_slippage.py b/tests/finance/test_slippage.py index 6934a17a..4d96e335 100644 --- a/tests/finance/test_slippage.py +++ b/tests/finance/test_slippage.py @@ -91,10 +91,10 @@ class SlippageTestCase(WithSimParams, WithDataPortal, ZiplineTestCase): start=normalize_date(self.minutes[0]), end=normalize_date(self.minutes[-1]) ) - with tmp_bcolz_equity_minute_bar_reader(self.trading_schedule, days, assets) \ + with tmp_bcolz_equity_minute_bar_reader(self.trading_calendar, days, assets) \ as reader: data_portal = DataPortal( - self.env.asset_finder, self.trading_schedule, + self.env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_minute_reader=reader, ) @@ -481,10 +481,10 @@ class SlippageTestCase(WithSimParams, WithDataPortal, ZiplineTestCase): start=normalize_date(self.minutes[0]), end=normalize_date(self.minutes[-1]) ) - with tmp_bcolz_equity_minute_bar_reader(self.trading_schedule, days, assets) \ + with tmp_bcolz_equity_minute_bar_reader(self.trading_calendar, days, assets) \ as reader: data_portal = DataPortal( - self.env.asset_finder, self.trading_schedule, + self.env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_minute_reader=reader, ) diff --git a/tests/pipeline/base.py b/tests/pipeline/base.py index 27e7d83d..31050bec 100644 --- a/tests/pipeline/base.py +++ b/tests/pipeline/base.py @@ -17,7 +17,7 @@ from zipline.testing import ( ExplodingObject, tmp_asset_finder, ) -from zipline.testing.fixtures import ZiplineTestCase, WithTradingSchedule +from zipline.testing.fixtures import ZiplineTestCase, WithTradingCalendar from zipline.utils.functional import dzip_exact from zipline.utils.pandas_utils import explode @@ -50,14 +50,14 @@ def with_defaults(**default_funcs): with_default_shape = with_defaults(shape=lambda self: self.default_shape) -class BasePipelineTestCase(WithTradingSchedule, ZiplineTestCase): +class BasePipelineTestCase(WithTradingCalendar, ZiplineTestCase): @classmethod def init_class_fixtures(cls): super(BasePipelineTestCase, cls).init_class_fixtures() cls.__calendar = date_range('2014', '2015', - freq=cls.trading_schedule.day) + freq=cls.trading_calendar.day) cls.__assets = assets = Int64Index(arange(1, 20)) cls.__tmp_finder_ctx = tmp_asset_finder( equities=make_simple_equity_info( diff --git a/tests/pipeline/test_engine.py b/tests/pipeline/test_engine.py index 0fe3839a..65fc950f 100644 --- a/tests/pipeline/test_engine.py +++ b/tests/pipeline/test_engine.py @@ -782,7 +782,7 @@ class FrameInputTestCase(WithTradingEnvironment, ZiplineTestCase): cls.dates = date_range( cls.start, cls.end, - freq=cls.trading_schedule.day, + freq=cls.trading_calendar.day, tz='UTC', ) cls.assets = cls.asset_finder.retrieve_all(cls.asset_ids) @@ -886,7 +886,7 @@ class SyntheticBcolzTestCase(WithAdjustmentReader, cls.equity_info = ret = make_rotating_equity_info( num_assets=6, first_start=cls.first_asset_start, - frequency=cls.trading_schedule.day, + frequency=cls.trading_calendar.day, periods_between_starts=4, asset_lifetime=8, ) @@ -941,15 +941,15 @@ class SyntheticBcolzTestCase(WithAdjustmentReader, def test_SMA(self): engine = SimplePipelineEngine( lambda column: self.pipeline_loader, - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, self.asset_finder, ) window_length = 5 asset_ids = self.all_asset_ids dates = date_range( - self.first_asset_start + self.trading_schedule.day, + self.first_asset_start + self.trading_calendar.day, self.last_asset_end, - freq=self.trading_schedule.day, + freq=self.trading_calendar.day, ) dates_to_test = dates[window_length:] @@ -969,7 +969,7 @@ class SyntheticBcolzTestCase(WithAdjustmentReader, # **previous** day's data. expected_raw = rolling_mean( expected_bar_values_2d( - dates - self.trading_schedule.day, + dates - self.trading_calendar.day, self.equity_info, 'close', ), @@ -995,15 +995,15 @@ class SyntheticBcolzTestCase(WithAdjustmentReader, # valuable. engine = SimplePipelineEngine( lambda column: self.pipeline_loader, - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, self.asset_finder, ) window_length = 5 asset_ids = self.all_asset_ids dates = date_range( - self.first_asset_start + self.trading_schedule.day, + self.first_asset_start + self.trading_calendar.day, self.last_asset_end, - freq=self.trading_schedule.day, + freq=self.trading_calendar.day, ) dates_to_test = dates[window_length:] @@ -1039,7 +1039,7 @@ class ParameterizedFactorTestCase(WithTradingEnvironment, ZiplineTestCase): @classmethod def init_class_fixtures(cls): super(ParameterizedFactorTestCase, cls).init_class_fixtures() - day = cls.trading_schedule.day + day = cls.trading_calendar.day cls.dates = dates = date_range( '2015-02-01', diff --git a/tests/pipeline/test_frameload.py b/tests/pipeline/test_frameload.py index f9e0a11c..57ec2091 100644 --- a/tests/pipeline/test_frameload.py +++ b/tests/pipeline/test_frameload.py @@ -24,22 +24,21 @@ from zipline.pipeline.data import USEquityPricing from zipline.pipeline.loaders.frame import ( DataFrameLoader, ) -from zipline.utils.calendars import default_nyse_schedule - - -trading_day = default_nyse_schedule.day +from zipline.utils.calendars import get_calendar class DataFrameLoaderTestCase(TestCase): def setUp(self): + self.trading_day = get_calendar("NYSE").day + self.nsids = 5 self.ndates = 20 self.sids = Int64Index(range(self.nsids)) self.dates = DatetimeIndex( start='2014-01-02', - freq=trading_day, + freq=self.trading_day, periods=self.ndates, ) @@ -161,17 +160,17 @@ class DataFrameLoaderTestCase(TestCase): }, { # Date Before Known Data 'sid': 2, - 'start_date': self.dates[0] - (2 * trading_day), - 'end_date': self.dates[0] - trading_day, - 'apply_date': self.dates[0] - trading_day, + 'start_date': self.dates[0] - (2 * self.trading_day), + 'end_date': self.dates[0] - self.trading_day, + 'apply_date': self.dates[0] - self.trading_day, 'value': -9999.0, 'kind': OVERWRITE, }, { # Date After Known Data 'sid': 2, - 'start_date': self.dates[-1] + trading_day, - 'end_date': self.dates[-1] + (2 * trading_day), - 'apply_date': self.dates[-1] + (3 * trading_day), + 'start_date': self.dates[-1] + self.trading_day, + 'end_date': self.dates[-1] + (2 * self.trading_day), + 'apply_date': self.dates[-1] + (3 * self.trading_day), 'value': -9999.0, 'kind': OVERWRITE, }, diff --git a/tests/pipeline/test_pipeline_algo.py b/tests/pipeline/test_pipeline_algo.py index edd074c1..bf176153 100644 --- a/tests/pipeline/test_pipeline_algo.py +++ b/tests/pipeline/test_pipeline_algo.py @@ -60,8 +60,7 @@ from zipline.testing.fixtures import ( WithDataPortal, ZiplineTestCase, ) -from zipline.utils.calendars import default_nyse_schedule - +from zipline.utils.calendars import get_calendar TEST_RESOURCE_PATH = join( dirname(dirname(realpath(__file__))), # zipline_repo/tests @@ -70,9 +69,6 @@ TEST_RESOURCE_PATH = join( ) -trading_day = default_nyse_schedule.day - - def rolling_vwap(df, length): "Simple rolling vwap implementation for testing" closes = df['close'].values @@ -90,7 +86,8 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): sids = 1, 2, 3 START_DATE = pd.Timestamp('2014-01-01', tz='utc') END_DATE = pd.Timestamp('2014-02-01', tz='utc') - dates = date_range(START_DATE, END_DATE, freq=trading_day, tz='utc') + dates = date_range(START_DATE, END_DATE, freq=get_calendar("NYSE").day, + tz='utc') @classmethod def make_equity_info(cls): @@ -145,9 +142,11 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): cls.last_asset_end = max(cls.equity_info.end_date) cls.assets = cls.asset_finder.retrieve_all(cls.sids) + cls.trading_day = cls.trading_calendar.day + # Add a split for 'A' on its second date. cls.split_asset = cls.assets[0] - cls.split_date = cls.split_asset.start_date + trading_day + cls.split_date = cls.split_asset.start_date + cls.trading_day cls.split_ratio = 0.5 cls.adjustments = DataFrame.from_records([ { @@ -199,8 +198,8 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): handle_data=late_attach, data_frequency='daily', get_pipeline_loader=lambda column: self.pipeline_loader, - start=self.first_asset_start - trading_day, - end=self.last_asset_end + trading_day, + start=self.first_asset_start - self.trading_day, + end=self.last_asset_end + self.trading_day, env=self.env, ) @@ -216,8 +215,8 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): handle_data=barf, data_frequency='daily', get_pipeline_loader=lambda column: self.pipeline_loader, - start=self.first_asset_start - trading_day, - end=self.last_asset_end + trading_day, + start=self.first_asset_start - self.trading_day, + end=self.last_asset_end + self.trading_day, env=self.env, ) @@ -245,8 +244,8 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): before_trading_start=before_trading_start, data_frequency='daily', get_pipeline_loader=lambda column: self.pipeline_loader, - start=self.first_asset_start - trading_day, - end=self.last_asset_end + trading_day, + start=self.first_asset_start - self.trading_day, + end=self.last_asset_end + self.trading_day, env=self.env, ) @@ -273,8 +272,8 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): before_trading_start=before_trading_start, data_frequency='daily', get_pipeline_loader=lambda column: self.pipeline_loader, - start=self.first_asset_start - trading_day, - end=self.last_asset_end + trading_day, + start=self.first_asset_start - self.trading_day, + end=self.last_asset_end + self.trading_day, env=self.env, ) @@ -308,7 +307,7 @@ class ClosesOnly(WithDataPortal, ZiplineTestCase): for asset in self.assets: # Assets should appear iff they exist today and yesterday. exists_today = self.exists(date, asset) - existed_yesterday = self.exists(date - trading_day, asset) + existed_yesterday = self.exists(date - self.trading_day, asset) if exists_today and existed_yesterday: latest = results.loc[asset, 'close'] self.assertEqual(latest, self.expected_close(date, asset)) @@ -437,7 +436,7 @@ class PipelineAlgorithmTestCase(WithBcolzEquityDailyBarReaderFromCSVs, raw_vwap[:split_loc - 1], adj_vwap[split_loc - 1:] ] - ).shift(1, trading_day) + ).shift(1, self.trading_calendar.day) # Make sure all the expected vwaps have the same dates. vwap_dates = vwaps[1][self.AAPL].index @@ -449,11 +448,13 @@ class PipelineAlgorithmTestCase(WithBcolzEquityDailyBarReaderFromCSVs, # Spot check expectations near the AAPL split. # length 1 vwap for the morning before the split should be the close # price of the previous day. - before_split = vwaps[1][AAPL].loc[split_date - trading_day] + before_split = vwaps[1][AAPL].loc[split_date - + self.trading_calendar.day] assert_almost_equal(before_split, 647.3499, decimal=2) assert_almost_equal( before_split, - raw[AAPL].loc[split_date - (2 * trading_day), 'close'], + raw[AAPL].loc[split_date - (2 * self.trading_calendar.day), + 'close'], decimal=2, ) @@ -463,13 +464,15 @@ class PipelineAlgorithmTestCase(WithBcolzEquityDailyBarReaderFromCSVs, assert_almost_equal(on_split, 645.5700 / split_ratio, decimal=2) assert_almost_equal( on_split, - raw[AAPL].loc[split_date - trading_day, 'close'] / split_ratio, + raw[AAPL].loc[split_date - + self.trading_calendar.day, 'close'] / split_ratio, decimal=2, ) # length 1 vwap on the day after the split should be the as-traded # close on the split day. - after_split = vwaps[1][AAPL].loc[split_date + trading_day] + after_split = vwaps[1][AAPL].loc[split_date + + self.trading_calendar.day] assert_almost_equal(after_split, 93.69999, decimal=2) assert_almost_equal( after_split, @@ -601,7 +604,7 @@ class PipelineAlgorithmTestCase(WithBcolzEquityDailyBarReaderFromCSVs, # For ensuring we call before_trading_start. count = [0] - current_day = default_nyse_schedule.next_execution_day( + current_day = self.trading_calendar.next_session_label( self.pipeline_loader.raw_price_loader.last_available_dt, ) diff --git a/tests/resources/calendars/nyse.csv b/tests/resources/calendars/nyse.csv index dce0c3a8..be75f652 100644 --- a/tests/resources/calendars/nyse.csv +++ b/tests/resources/calendars/nyse.csv @@ -6616,4 +6616,4 @@ 2016-04-01 00:00:00+00:00,2016-04-01 13:31:00+00:00,2016-04-01 20:00:00+00:00 2016-04-04 00:00:00+00:00,2016-04-04 13:31:00+00:00,2016-04-04 20:00:00+00:00 2016-04-05 00:00:00+00:00,2016-04-05 13:31:00+00:00,2016-04-05 20:00:00+00:00 -2016-04-06 00:00:00+00:00,2016-04-06 13:31:00+00:00,2016-04-06 20:00:00+00:00 +2016-04-06 00:00:00+00:00,2016-04-06 13:31:00+00:00,2016-04-06 20:00:00+00:00 \ No newline at end of file diff --git a/tests/resources/yahoo_samples/rebuild_samples b/tests/resources/yahoo_samples/rebuild_samples index 8111e9b2..c493cac1 100644 --- a/tests/resources/yahoo_samples/rebuild_samples +++ b/tests/resources/yahoo_samples/rebuild_samples @@ -27,7 +27,7 @@ def pricing_for_sid(sid): def column(name): return np.arange(252) + 1 + sid * 10000 + modifier[name] * 1000 - trading_days = get_calendar('NYSE').all_trading_days + trading_days = get_calendar('NYSE').all_sessions return pd.DataFrame( data={ diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index c89677be..192c6a68 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -1,5 +1,5 @@ # -# Copyright 2015 Quantopian, Inc. +# Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,9 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import datetime import numpy as np -import pytz +import pandas as pd import zipline.finance.risk as risk from zipline.utils import factory @@ -31,20 +30,13 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def init_instance_fixtures(self): super(TestRisk, self).init_instance_fixtures() - start_date = datetime.datetime( - year=2006, - month=1, - day=1, - hour=0, - minute=0, - tzinfo=pytz.utc) - end_date = datetime.datetime( - year=2006, month=12, day=29, tzinfo=pytz.utc) + start_session = pd.Timestamp("2006-01-01", tz='UTC') + end_session = pd.Timestamp("2006-12-29", tz='UTC') self.sim_params = SimulationParameters( - period_start=start_date, - period_end=end_date, - trading_schedule=self.trading_schedule, + start_session=start_session, + end_session=end_session, + trading_calendar=self.trading_calendar, ) self.algo_returns_06 = factory.create_returns_from_list( @@ -55,7 +47,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): self.cumulative_metrics_06 = risk.RiskMetricsCumulative( self.sim_params, treasury_curves=self.env.treasury_curves, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) for dt, returns in answer_key.RETURNS_DATA.iterrows(): diff --git a/tests/risk/test_risk_period.py b/tests/risk/test_risk_period.py index b230d01d..2ae9e750 100644 --- a/tests/risk/test_risk_period.py +++ b/tests/risk/test_risk_period.py @@ -1,5 +1,5 @@ # -# Copyright 2013 Quantopian, Inc. +# Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ import datetime import calendar +import pandas as pd import numpy as np import pytz @@ -39,20 +40,17 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def init_instance_fixtures(self): super(TestRisk, self).init_instance_fixtures() - start_date = datetime.datetime( - year=2006, - month=1, - day=1, - hour=0, - minute=0, - tzinfo=pytz.utc) - end_date = datetime.datetime( - year=2006, month=12, day=31, tzinfo=pytz.utc) + start_session = pd.Timestamp("2006-01-01", tz='UTC') + + end_session = self.trading_calendar.minute_to_session_label( + pd.Timestamp("2006-12-31", tz='UTC'), + direction="previous" + ) self.sim_params = SimulationParameters( - period_start=start_date, - period_end=end_date, - trading_schedule=self.trading_schedule, + start_session=start_session, + end_session=end_session, + trading_calendar=self.trading_calendar, ) self.algo_returns_06 = factory.create_returns_from_list( @@ -67,28 +65,14 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): self.algo_returns_06, self.sim_params, benchmark_returns=self.benchmark_returns_06, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, ) - start_08 = datetime.datetime( - year=2008, - month=1, - day=1, - hour=0, - minute=0, - tzinfo=pytz.utc) - - end_08 = datetime.datetime( - year=2008, - month=12, - day=31, - tzinfo=pytz.utc - ) self.sim_params08 = SimulationParameters( - period_start=start_08, - period_end=end_08, - trading_schedule=self.trading_schedule, + start_session=pd.Timestamp("2008-01-01", tz='UTC'), + end_session=pd.Timestamp("2008-12-31", tz='UTC'), + trading_calendar=self.trading_calendar, ) def test_factory(self): @@ -106,7 +90,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): returns.index[0], returns.index[-1], returns, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, benchmark_returns=self.env.benchmark_returns, treasury_curves=self.env.treasury_curves, ) @@ -134,7 +118,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def test_trading_days_06(self): returns = factory.create_returns_from_range(self.sim_params) metrics = risk.RiskReport(returns, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, benchmark_returns=self.env.benchmark_returns) self.assertEqual([x.num_trading_days for x in metrics.year_periods], @@ -361,7 +345,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def test_benchmark_returns_08(self): returns = factory.create_returns_from_range(self.sim_params08) metrics = risk.RiskReport(returns, self.sim_params08, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, benchmark_returns=self.env.benchmark_returns) @@ -410,7 +394,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def test_trading_days_08(self): returns = factory.create_returns_from_range(self.sim_params08) metrics = risk.RiskReport(returns, self.sim_params08, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, benchmark_returns=self.env.benchmark_returns) self.assertEqual([x.num_trading_days for x in metrics.year_periods], @@ -422,7 +406,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def test_benchmark_volatility_08(self): returns = factory.create_returns_from_range(self.sim_params08) metrics = risk.RiskReport(returns, self.sim_params08, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, benchmark_returns=self.env.benchmark_returns) @@ -473,7 +457,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def test_treasury_returns_06(self): returns = factory.create_returns_from_range(self.sim_params) metrics = risk.RiskReport(returns, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, benchmark_returns=self.env.benchmark_returns) self.assertEqual([round(x.treasury_period_return, 4) @@ -518,52 +502,55 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): [0.0500]) def test_benchmarkrange(self): - self.check_year_range( - datetime.datetime( - year=2008, month=1, day=1, tzinfo=pytz.utc), - 2) + start_session = self.trading_calendar.minute_to_session_label( + pd.Timestamp("2008-01-01", tz='UTC') + ) + + end_session = self.trading_calendar.minute_to_session_label( + pd.Timestamp("2010-01-01", tz='UTC'), direction="previous" + ) + + sim_params = SimulationParameters( + start_session=start_session, + end_session=end_session, + trading_calendar=self.trading_calendar, + ) + + returns = factory.create_returns_from_range(sim_params) + metrics = risk.RiskReport(returns, self.sim_params, + trading_calendar=self.trading_calendar, + treasury_curves=self.env.treasury_curves, + benchmark_returns=self.env.benchmark_returns) + + self.check_metrics(metrics, 24, start_session) + # self.check_year_range( + # datetime.datetime( + # year=2008, month=1, day=1, tzinfo=pytz.utc), + # 2) def test_partial_month(self): - start = datetime.datetime( - year=1991, - month=1, - day=1, - hour=0, - minute=0, - tzinfo=pytz.utc) + start_session = self.trading_calendar.minute_to_session_label( + pd.Timestamp("1991-01-01", tz='UTC') + ) # 1992 and 1996 were leap years total_days = 365 * 5 + 2 - end = start + datetime.timedelta(days=total_days) + end_session = start_session + datetime.timedelta(days=total_days) sim_params90s = SimulationParameters( - period_start=start, - period_end=end, - trading_schedule=self.trading_schedule, + start_session=start_session, + end_session=end_session, + trading_calendar=self.trading_calendar, ) returns = factory.create_returns_from_range(sim_params90s) returns = returns[:-10] # truncate the returns series to end mid-month metrics = risk.RiskReport(returns, sim_params90s, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, benchmark_returns=self.env.benchmark_returns) total_months = 60 - self.check_metrics(metrics, total_months, start) - - def check_year_range(self, start_date, years): - sim_params = SimulationParameters( - period_start=start_date, - period_end=start_date.replace(year=(start_date.year + years)), - trading_schedule=self.trading_schedule, - ) - returns = factory.create_returns_from_range(sim_params) - metrics = risk.RiskReport(returns, self.sim_params, - trading_schedule=self.trading_schedule, - treasury_curves=self.env.treasury_curves, - benchmark_returns=self.env.benchmark_returns) - total_months = years * 12 - self.check_metrics(metrics, total_months, start_date) + self.check_metrics(metrics, total_months, start_session) def check_metrics(self, metrics, total_months, start_date): """ @@ -621,7 +608,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): def assert_range_length(self, col, total_months, period_length, start_date): - if(period_length > total_months): + if (period_length > total_months): self.assertEqual(len(col), 0) else: self.assertEqual( @@ -633,11 +620,11 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): calculated end:{end}".format(total_months=total_months, period_length=period_length, start_date=start_date, - end=col[-1].end_date, + end=col[-1]._end_session, actual=len(col)) ) - self.assert_month(start_date.month, col[-1].end_date.month) - self.assert_last_day(col[-1].end_date) + self.assert_month(start_date.month, col[-1]._end_session.month) + self.assert_last_day(col[-1]._end_session) def test_sparse_benchmark(self): benchmark_returns = self.benchmark_returns_06.copy() @@ -648,7 +635,7 @@ class TestRisk(WithTradingEnvironment, ZiplineTestCase): self.algo_returns_06, self.sim_params, benchmark_returns=benchmark_returns, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.env.treasury_curves, ) for risk_period in chain.from_iterable(itervalues(report.to_dict())): diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 5ebe580f..c24005d0 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -96,7 +96,7 @@ from zipline.testing.fixtures import ( WithSimParams, WithTradingEnvironment, WithTmpDir, - WithTradingSchedule, + WithTradingCalendar, ZiplineTestCase, ) from zipline.test_algorithms import ( @@ -313,7 +313,6 @@ def handle_data(context, data): aapl_dt = data.current(sid(1), "last_traded") assert_equal(aapl_dt, get_datetime()) """ - algo = TradingAlgorithm(script=algo_text, sim_params=self.sim_params, env=self.env) @@ -533,31 +532,48 @@ def handle_data(context, data): self.assertIs(composer, zipline.utils.events.ComposedRule.lazy_and) def test_asset_lookup(self): - algo = TradingAlgorithm(env=self.env) + # this date doesn't matter + start_session = pd.Timestamp("2000-01-01", tz="UTC") + # Test before either PLAY existed - algo.sim_params.period_end = pd.Timestamp('2001-12-01', tz='UTC') + algo.sim_params = algo.sim_params.create_new( + start_session, + pd.Timestamp('2001-12-01', tz='UTC') + ) with self.assertRaises(SymbolNotFound): algo.symbol('PLAY') with self.assertRaises(SymbolNotFound): algo.symbols('PLAY') # Test when first PLAY exists - algo.sim_params.period_end = pd.Timestamp('2002-12-01', tz='UTC') + algo.sim_params = algo.sim_params.create_new( + start_session, + pd.Timestamp('2002-12-01', tz='UTC') + ) list_result = algo.symbols('PLAY') self.assertEqual(3, list_result[0]) # Test after first PLAY ends - algo.sim_params.period_end = pd.Timestamp('2004-12-01', tz='UTC') + algo.sim_params = algo.sim_params.create_new( + start_session, + pd.Timestamp('2004-12-01', tz='UTC') + ) self.assertEqual(3, algo.symbol('PLAY')) # Test after second PLAY begins - algo.sim_params.period_end = pd.Timestamp('2005-12-01', tz='UTC') + algo.sim_params = algo.sim_params.create_new( + start_session, + pd.Timestamp('2005-12-01', tz='UTC') + ) self.assertEqual(4, algo.symbol('PLAY')) # Test after second PLAY ends - algo.sim_params.period_end = pd.Timestamp('2006-12-01', tz='UTC') + algo.sim_params = algo.sim_params.create_new( + start_session, + pd.Timestamp('2006-12-01', tz='UTC') + ) self.assertEqual(4, algo.symbol('PLAY')) list_result = algo.symbols('PLAY') self.assertEqual(4, list_result[0]) @@ -710,7 +726,10 @@ def handle_data(context, data): # Set the period end to a date after the period end # dates for our assets. - algo.sim_params.period_end = pd.Timestamp('2015-01-01', tz='UTC') + algo.sim_params = algo.sim_params.create_new( + algo.sim_params.start_session, + pd.Timestamp('2015-01-01', tz='UTC') + ) # With no symbol lookup date set, we will use the period end date # for the as_of_date, resulting here in the asset with the earlier @@ -753,10 +772,10 @@ class TestTransformAlgorithm(WithLogger, [100, 100, 100, 300], timedelta(days=1), cls.sim_params, - cls.trading_schedule, + cls.trading_calendar, ) for sid in cls.sids }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) @classmethod @@ -914,9 +933,10 @@ def before_trading_start(context, data): asset133 = self.env.asset_finder.retrieve_asset(133) sim_params = SimulationParameters( - period_start=asset133.start_date, - period_end=asset133.end_date, - data_frequency="minute" + start_session=asset133.start_date, + end_session=asset133.end_date, + data_frequency="minute", + trading_calendar=self.trading_calendar ) algo = TradingAlgorithm( @@ -942,20 +962,20 @@ def before_trading_start(context, data): (TestOrderPercentAlgorithm,) ]) def test_minute_data(self, algo_class): - period_start = pd.Timestamp('2002-1-2', tz='UTC') + start_session = pd.Timestamp('2002-1-2', tz='UTC') period_end = pd.Timestamp('2002-1-4', tz='UTC') equities = pd.DataFrame([{ - 'start_date': period_start, + 'start_date': start_session, 'end_date': period_end + timedelta(days=1) }] * 2) with TempDirectory() as tempdir, \ tmp_trading_env(equities=equities) as env: sim_params = SimulationParameters( - period_start=period_start, - period_end=period_end, + start_session=start_session, + end_session=period_end, capital_base=float("1.0e5"), data_frequency='minute', - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal( @@ -963,7 +983,7 @@ def before_trading_start(context, data): tempdir, sim_params, equities.index, - self.trading_schedule, + self.trading_calendar, ) algo = algo_class(sim_params=sim_params, env=env) algo.run(data_portal) @@ -1015,6 +1035,9 @@ class TestBeforeTradingStart(WithDataPortal, SIM_PARAMS_DATA_FREQUENCY = 'minute' EQUITY_DAILY_BAR_LOOKBACK_DAYS = EQUITY_MINUTE_BAR_LOOKBACK_DAYS = 1 + DATA_PORTAL_FIRST_TRADING_DAY = pd.Timestamp("2016-01-05", tz='UTC') + EQUITY_MINUTE_BAR_START_DATE = pd.Timestamp("2016-01-05", tz='UTC') + data_start = ASSET_FINDER_EQUITY_START_DATE = pd.Timestamp( '2016-01-05', tz='utc', @@ -1026,7 +1049,7 @@ class TestBeforeTradingStart(WithDataPortal, @classmethod def make_equity_minute_bar_data(cls): asset_minutes = \ - cls.trading_schedule.execution_minutes_for_days_in_range( + cls.trading_calendar.minutes_in_range( cls.data_start, cls.END_DATE, ) @@ -1045,15 +1068,15 @@ class TestBeforeTradingStart(WithDataPortal, split_data.iloc[780:] = split_data.iloc[780:] / 2.0 for sid in (1, 8554): yield sid, create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.data_start, - cls.sim_params.period_end, + cls.sim_params.end_session, ) yield 2, create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.data_start, - cls.sim_params.period_end, + cls.sim_params.end_session, 50, ) yield cls.SPLIT_ASSET_SID, split_data @@ -1072,9 +1095,9 @@ class TestBeforeTradingStart(WithDataPortal, def make_equity_daily_bar_data(cls): for sid in cls.ASSET_FINDER_EQUITY_SIDS: yield sid, create_daily_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.data_start, - cls.sim_params.period_end, + cls.sim_params.end_session, ) def test_data_in_bts_minute(self): @@ -1253,7 +1276,7 @@ class TestBeforeTradingStart(WithDataPortal, if not context.ordered: order(sid(1), 1) context.ordered = True - context.hd_acount = context.account + context.hd_account = context.account """) algo = TradingAlgorithm( @@ -1410,14 +1433,14 @@ class TestAlgoScript(WithLogger, [100] * days, timedelta(days=1), cls.sim_params, - cls.trading_schedule), + cls.trading_calendar), 3: factory.create_trade_history( 3, [10.0] * days, [100] * days, timedelta(days=1), cls.sim_params, - cls.trading_schedule) + cls.trading_calendar) }, index=cls.equity_daily_bar_days, ) @@ -1556,9 +1579,9 @@ def handle_data(context, data): env=self.env, ) trades = factory.create_daily_trade_source( - [0], self.sim_params, self.env, self.trading_schedule) + [0], self.sim_params, self.env, self.trading_calendar) data_portal = create_data_portal_from_trade_history( - self.env.asset_finder, self.trading_schedule, tempdir, + self.env.asset_finder, self.trading_calendar, tempdir, self.sim_params, {0: trades}) results = test_algo.run(data_portal) @@ -1644,9 +1667,9 @@ def handle_data(context, data): def test_order_dead_asset(self): # after asset 0 is dead params = SimulationParameters( - period_start=pd.Timestamp("2007-01-03", tz='UTC'), - period_end=pd.Timestamp("2007-01-05", tz='UTC'), - trading_schedule=self.trading_schedule, + start_session=pd.Timestamp("2007-01-03", tz='UTC'), + end_session=pd.Timestamp("2007-01-05", tz='UTC'), + trading_calendar=self.trading_calendar, ) # order method shouldn't blow up @@ -1725,9 +1748,15 @@ def handle_data(context, data): Test that api methods on the data object can be called with positional arguments. """ + params = SimulationParameters( + start_session=pd.Timestamp("2006-01-10", tz='UTC'), + end_session=pd.Timestamp("2006-01-11", tz='UTC'), + trading_calendar=self.trading_calendar, + ) + test_algo = TradingAlgorithm( script=call_without_kwargs, - sim_params=self.sim_params, + sim_params=params, env=self.env, ) test_algo.run(self.data_portal) @@ -1737,9 +1766,15 @@ def handle_data(context, data): Test that api methods on the data object can be called with keyword arguments. """ + params = SimulationParameters( + start_session=pd.Timestamp("2006-01-10", tz='UTC'), + end_session=pd.Timestamp("2006-01-11", tz='UTC'), + trading_calendar=self.trading_calendar, + ) + test_algo = TradingAlgorithm( script=call_with_kwargs, - sim_params=self.sim_params, + sim_params=params, env=self.env, ) test_algo.run(self.data_portal) @@ -1785,6 +1820,12 @@ def handle_data(context, data): self.assertEqual(expected, cm.exception.args[0]) def test_empty_asset_list_to_history(self): + params = SimulationParameters( + start_session=pd.Timestamp("2006-01-10", tz='UTC'), + end_session=pd.Timestamp("2006-01-11", tz='UTC'), + trading_calendar=self.trading_calendar, + ) + algo = TradingAlgorithm( script=dedent(""" def initialize(context): @@ -1793,7 +1834,7 @@ def handle_data(context, data): def handle_data(context, data): data.history([], "price", 5, '1d') """), - sim_params=self.sim_params, + sim_params=params, env=self.env ) @@ -1946,7 +1987,7 @@ class TestCapitalChanges(WithLogger, @classmethod def make_equity_minute_bar_data(cls): - minutes = cls.trading_schedule.execution_minutes_for_days_in_range( + minutes = cls.trading_calendar.minutes_in_range( pd.Timestamp('2006-01-03', tz='UTC'), pd.Timestamp('2006-01-09', tz='UTC') ) @@ -1958,14 +1999,14 @@ class TestCapitalChanges(WithLogger, [10000] * len(minutes), timedelta(minutes=1), cls.sim_params, - cls.trading_schedule), + cls.trading_calendar), }, index=pd.DatetimeIndex(minutes), ) @classmethod def make_equity_daily_bar_data(cls): - days = cls.trading_schedule.execution_days_in_range( + days = cls.trading_calendar.minutes_in_range( pd.Timestamp('2006-01-03', tz='UTC'), pd.Timestamp('2006-01-09', tz='UTC') ) @@ -1977,7 +2018,7 @@ class TestCapitalChanges(WithLogger, [10000] * len(days), timedelta(days=1), cls.sim_params, - cls.trading_schedule), + cls.trading_calendar), }, index=pd.DatetimeIndex(days), ) @@ -2733,7 +2774,7 @@ class TestTradingControls(WithSimParams, WithDataPortal, ZiplineTestCase): tempdir, sim_params, [1], - self.trading_schedule, + self.trading_calendar, ) def handle_data(algo, data): @@ -2841,7 +2882,7 @@ class TestTradingControls(WithSimParams, WithDataPortal, ZiplineTestCase): def test_asset_date_bounds(self): metadata = pd.DataFrame([{ - 'start_date': self.sim_params.period_start, + 'start_date': self.sim_params.start_session, 'end_date': '2020-01-01', }]) with TempDirectory() as tempdir, \ @@ -2855,7 +2896,7 @@ class TestTradingControls(WithSimParams, WithDataPortal, ZiplineTestCase): tempdir, self.sim_params, [0], - self.trading_schedule, + self.trading_calendar, ) algo.run(data_portal) @@ -2870,7 +2911,7 @@ class TestTradingControls(WithSimParams, WithDataPortal, ZiplineTestCase): tempdir, self.sim_params, [0], - self.trading_schedule, + self.trading_calendar, ) algo = SetAssetDateBoundsAlgorithm( sim_params=self.sim_params, @@ -2890,7 +2931,7 @@ class TestTradingControls(WithSimParams, WithDataPortal, ZiplineTestCase): tempdir, self.sim_params, [0], - self.trading_schedule, + self.trading_calendar, ) algo = SetAssetDateBoundsAlgorithm( sim_params=self.sim_params, @@ -2916,10 +2957,10 @@ class TestAccountControls(WithDataPortal, WithSimParams, ZiplineTestCase): [100, 100, 100, 300], timedelta(days=1), cls.sim_params, - cls.trading_schedule, + cls.trading_calendar, ), }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) def _check_algo(self, @@ -3063,18 +3104,19 @@ class TestFutureFlip(WithDataPortal, WithSimParams, ZiplineTestCase): [1e9, 1e9], timedelta(days=1), cls.sim_params, - cls.trading_schedule, + cls.trading_calendar, ), }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) @skip('broken in zipline 1.0.0') def test_flip_algo(self): metadata = {1: {'symbol': 'TEST', 'start_date': self.sim_params.trading_days[0], - 'end_date': self.trading_schedule.next_execution_day( - self.sim_params.trading_days[-1]), + 'end_date': self.trading_calendar.next_session_label( + self.sim_params.sessions[-1] + ), 'multiplier': 5}} self.env.write_data(futures_data=metadata) @@ -3174,9 +3216,9 @@ class TestOrderCancelation(WithDataPortal, @classmethod def make_equity_minute_bar_data(cls): asset_minutes = \ - cls.trading_schedule.execution_minutes_for_days_in_range( - cls.sim_params.period_start, - cls.sim_params.period_end, + cls.trading_calendar.minutes_for_sessions_in_range( + cls.sim_params.start_session, + cls.sim_params.end_session, ) minutes_count = len(asset_minutes) @@ -3204,7 +3246,7 @@ class TestOrderCancelation(WithDataPortal, 'close': np.full(3, 1), 'volume': np.full(3, 1), }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) def prep_algo(self, cancelation_string, data_frequency="minute", @@ -3214,9 +3256,9 @@ class TestOrderCancelation(WithDataPortal, script=code, env=self.env, sim_params=SimulationParameters( - period_start=self.sim_params.period_start, - period_end=self.sim_params.period_end, - trading_schedule=self.trading_schedule, + start_session=self.sim_params.start_session, + end_session=self.sim_params.end_session, + trading_calendar=self.trading_calendar, data_frequency=data_frequency, emission_rate='minute' if minute_emission else 'daily' ) @@ -3329,7 +3371,7 @@ class TestOrderCancelation(WithDataPortal, self.assertFalse(log_catcher.has_warnings) -class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): +class TestEquityAutoClose(WithTmpDir, WithTradingCalendar, ZiplineTestCase): """ Tests if delisted equities are properly removed from a portfolio holding positions in said equities. @@ -3337,11 +3379,11 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): @classmethod def init_class_fixtures(cls): super(TestEquityAutoClose, cls).init_class_fixtures() - trading_days = cls.trading_schedule.all_execution_days + trading_sessions = cls.trading_calendar.all_sessions start_date = pd.Timestamp('2015-01-05', tz='UTC') - start_date_loc = trading_days.get_loc(start_date) + start_date_loc = trading_sessions.get_loc(start_date) test_duration = 7 - cls.test_days = trading_days[ + cls.test_days = trading_sessions[ start_date_loc:start_date_loc + test_duration ] cls.first_asset_expiration = cls.test_days[2] @@ -3353,7 +3395,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): num_assets=3, start_date=self.test_days[0], first_end=self.first_asset_expiration, - frequency=self.trading_schedule.day, + frequency=self.trading_calendar.day, periods_between_ends=2, auto_close_delta=auto_close_delta, ) @@ -3361,10 +3403,10 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): sids = asset_info.index env = self.enter_instance_context(tmp_trading_env(equities=asset_info)) - market_opens = self.trading_schedule.schedule.market_open.loc[ + market_opens = self.trading_calendar.schedule.market_open.loc[ self.test_days ] - market_closes = self.trading_schedule.schedule.market_close.loc[ + market_closes = self.trading_calendar.schedule.market_close.loc[ self.test_days ] @@ -3382,17 +3424,17 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): frequency=frequency ) path = self.tmpdir.getpath("testdaily.bcolz") - BcolzDailyBarWriter(path, dates).write( + BcolzDailyBarWriter(path, dates, self.trading_calendar).write( iteritems(trade_data_by_sid), ) reader = BcolzDailyBarReader(path) data_portal = DataPortal( - env.asset_finder, self.trading_schedule, + env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_daily_reader=reader, ) elif frequency == 'minute': - dates = self.trading_schedule.execution_minutes_for_days_in_range( + dates = self.trading_calendar.minutes_for_sessions_in_range( self.test_days[0], self.test_days[-1], ) @@ -3417,7 +3459,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): ) reader = BcolzMinuteBarReader(self.tmpdir.path) data_portal = DataPortal( - env.asset_finder, self.trading_schedule, + env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_minute_reader=reader, ) @@ -3443,7 +3485,9 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): else: final_prices = { asset.sid: trade_data_by_sid[asset.sid].loc[ - self.trading_schedule.start_and_end(asset.end_date)[1] + self.trading_calendar.open_and_close_for_session( + asset.end_date + )[1] ].close for asset in assets } @@ -3515,7 +3559,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): Make sure that after an equity gets delisted, our portfolio holds the correct number of equities and correct amount of cash. """ - auto_close_delta = self.trading_schedule.day * auto_close_lag + auto_close_delta = self.trading_calendar.day * auto_close_lag resources = self.make_data(auto_close_delta, 'daily', capital_base) assets = resources.assets @@ -3594,7 +3638,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): # Check expected long/short counts. # We have longs if order_size > 0. - # We have shrots if order_size < 0. + # We have shrots if order_size > 0. self.assertEqual(algo.num_positions, expected_num_positions) if order_size > 0: self.assertEqual( @@ -3675,7 +3719,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): canceled. Unless an equity is auto closed, any open orders for that equity will persist indefinitely. """ - auto_close_delta = self.trading_schedule.day + auto_close_delta = self.trading_calendar.day resources = self.make_data(auto_close_delta, 'daily') env = resources.env assets = resources.assets @@ -3747,7 +3791,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingSchedule, ZiplineTestCase): ) def test_minutely_delisted_equities(self): - resources = self.make_data(self.trading_schedule.day, 'minute') + resources = self.make_data(self.trading_calendar.day, 'minute') env = resources.env assets = resources.assets @@ -3933,9 +3977,9 @@ class TestOrderAfterDelist(WithTradingEnvironment, ZiplineTestCase): script=algo_code, env=self.env, sim_params=SimulationParameters( - period_start=pd.Timestamp("2016-01-06", tz='UTC'), - period_end=pd.Timestamp("2016-01-07", tz='UTC'), - trading_schedule=self.trading_schedule, + start_session=pd.Timestamp("2016-01-06", tz='UTC'), + end_session=pd.Timestamp("2016-01-07", tz='UTC'), + trading_calendar=self.trading_calendar, data_frequency="minute" ) ) diff --git a/tests/test_api_shim.py b/tests/test_api_shim.py index 7f3dcb77..cb9a09c8 100644 --- a/tests/test_api_shim.py +++ b/tests/test_api_shim.py @@ -124,7 +124,7 @@ class TestAPIShim(WithDataPortal, WithSimParams, ZiplineTestCase): def make_equity_minute_bar_data(cls): for sid in cls.sids: yield sid, create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.SIM_PARAMS_START, cls.SIM_PARAMS_END, ) @@ -133,7 +133,7 @@ class TestAPIShim(WithDataPortal, WithSimParams, ZiplineTestCase): def make_equity_daily_bar_data(cls): for sid in cls.sids: yield sid, create_daily_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.SIM_PARAMS_START, cls.SIM_PARAMS_END, ) @@ -179,11 +179,11 @@ class TestAPIShim(WithDataPortal, WithSimParams, ZiplineTestCase): similar) and the new data API(data.current(sid(N), field) and similar) hit the same code paths on the DataPortal. """ - test_start_minute = self.trading_schedule.execution_minutes_for_day( - self.sim_params.trading_days[0] + test_start_minute = self.trading_calendar.minutes_for_session( + self.sim_params.sessions[0] )[1] - test_end_minute = self.trading_schedule.execution_minutes_for_day( - self.sim_params.trading_days[0] + test_end_minute = self.trading_calendar.minutes_for_session( + self.sim_params.sessions[0] )[-1] bar_data = BarData( self.data_portal, @@ -257,10 +257,10 @@ class TestAPIShim(WithDataPortal, WithSimParams, ZiplineTestCase): ) test_sim_params = SimulationParameters( - period_start=test_start_minute, - period_end=test_end_minute, + start_session=test_start_minute, + end_session=test_end_minute, data_frequency="minute", - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) history_algorithm = self.create_algo( @@ -375,13 +375,9 @@ class TestAPIShim(WithDataPortal, WithSimParams, ZiplineTestCase): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("default", ZiplineDeprecationWarning) - sim_params = SimulationParameters( - period_start=self.sim_params.trading_days[1], - period_end=self.sim_params.period_end, - capital_base=self.sim_params.capital_base, - data_frequency=self.sim_params.data_frequency, - emission_rate=self.sim_params.emission_rate, - trading_schedule=self.trading_schedule, + sim_params = self.sim_params.create_new( + self.sim_params.sessions[1], + self.sim_params.end_session ) algo = self.create_algo(history_algo, @@ -421,10 +417,10 @@ class TestAPIShim(WithDataPortal, WithSimParams, ZiplineTestCase): warnings.simplefilter("default", ZiplineDeprecationWarning) sim_params = SimulationParameters( - period_start=self.sim_params.trading_days[8], - period_end=self.sim_params.trading_days[-1], + start_session=self.sim_params.sessions[8], + end_session=self.sim_params.sessions[-1], data_frequency="minute", - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) algo = self.create_algo(simple_transforms_algo, diff --git a/tests/test_assets.py b/tests/test_assets.py index 44b347cf..d0819863 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -82,7 +82,7 @@ from zipline.testing.predicates import assert_equal from zipline.testing.fixtures import ( WithAssetFinder, ZiplineTestCase, - WithTradingSchedule, + WithTradingCalendar, ) @@ -396,7 +396,7 @@ class TestFuture(WithAssetFinder, ZiplineTestCase): TestFuture.asset_finder.lookup_future_symbol('XXX99') -class AssetFinderTestCase(WithTradingSchedule, ZiplineTestCase): +class AssetFinderTestCase(WithTradingCalendar, ZiplineTestCase): asset_finder_type = AssetFinder def write_assets(self, **kwargs): @@ -776,7 +776,7 @@ class AssetFinderTestCase(WithTradingSchedule, ZiplineTestCase): def test_compute_lifetimes(self): num_assets = 4 - trading_day = self.trading_schedule.day + trading_day = self.trading_calendar.day first_start = pd.Timestamp('2015-04-01', tz='UTC') frame = make_rotating_equity_info( diff --git a/tests/test_bar_data.py b/tests/test_bar_data.py index e151047d..f3d253cb 100644 --- a/tests/test_bar_data.py +++ b/tests/test_bar_data.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from datetime import timedelta from nose_parameterized import parameterized import numpy as np import pandas as pd @@ -110,21 +111,21 @@ class TestMinuteBarData(WithBarDataChecks, # illiquid_split_asset trades every 10 minutes for sid in (1, cls.SPLIT_ASSET_SID): yield sid, create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.equity_minute_bar_days[0], cls.equity_minute_bar_days[-1], ) for sid in (2, cls.ILLIQUID_SPLIT_ASSET_SID): yield sid, create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.equity_minute_bar_days[0], cls.equity_minute_bar_days[-1], 10, ) yield cls.HILARIOUSLY_ILLIQUID_ASSET_SID, create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.equity_minute_bar_days[0], cls.equity_minute_bar_days[-1], 50, @@ -165,8 +166,8 @@ class TestMinuteBarData(WithBarDataChecks, def test_minute_before_assets_trading(self): # grab minutes that include the day before the asset start - minutes = self.trading_schedule.execution_minutes_for_day( - self.trading_schedule.previous_execution_day( + minutes = self.trading_calendar.minutes_for_session( + self.trading_calendar.previous_session_label( self.equity_minute_bar_days[0] ) ) @@ -194,7 +195,7 @@ class TestMinuteBarData(WithBarDataChecks, self.assertTrue(asset_value is pd.NaT) def test_regular_minute(self): - minutes = self.trading_schedule.execution_minutes_for_day( + minutes = self.trading_calendar.minutes_for_session( self.equity_minute_bar_days[0] ) @@ -282,11 +283,13 @@ class TestMinuteBarData(WithBarDataChecks, self.assertEqual(minute, asset2_value) else: last_traded_minute = minutes[(idx // 10) * 10] - self.assertEqual(last_traded_minute - 1, - asset2_value) + self.assertEqual( + last_traded_minute - timedelta(minutes=1), + asset2_value + ) def test_minute_of_last_day(self): - minutes = self.trading_schedule.execution_minutes_for_day( + minutes = self.trading_calendar.minutes_for_session( self.equity_daily_bar_days[-1], ) @@ -298,13 +301,13 @@ class TestMinuteBarData(WithBarDataChecks, self.assertTrue(bar_data.can_trade(self.ASSET2)) def test_minute_after_assets_stopped(self): - minutes = self.trading_schedule.execution_minutes_for_day( - self.trading_schedule.next_execution_day( + minutes = self.trading_calendar.minutes_for_session( + self.trading_calendar.next_session_label( self.equity_minute_bar_days[-1] ) ) - last_trading_minute = self.trading_schedule.execution_minutes_for_day( + last_trading_minute = self.trading_calendar.minutes_for_session( self.equity_minute_bar_days[-1] )[-1] @@ -346,9 +349,9 @@ class TestMinuteBarData(WithBarDataChecks, ) # ... but that's it's not applied when using spot value - minutes = self.trading_schedule.execution_minutes_for_days_in_range( - start=self.equity_minute_bar_days[0], - end=self.equity_minute_bar_days[1] + minutes = self.trading_calendar.minutes_for_sessions_in_range( + self.equity_minute_bar_days[0], + self.equity_minute_bar_days[1] ) for idx, minute in enumerate(minutes): @@ -361,10 +364,10 @@ class TestMinuteBarData(WithBarDataChecks, def test_spot_price_is_adjusted_if_needed(self): # on cls.days[1], the first 9 minutes of ILLIQUID_SPLIT_ASSET are # missing. let's get them. - day0_minutes = self.trading_schedule.execution_minutes_for_day( + day0_minutes = self.trading_calendar.minutes_for_session( self.equity_minute_bar_days[0] ) - day1_minutes = self.trading_schedule.execution_minutes_for_day( + day1_minutes = self.trading_calendar.minutes_for_session( self.equity_minute_bar_days[1] ) @@ -438,7 +441,7 @@ class TestMinuteBarData(WithBarDataChecks, def test_can_trade_at_midnight(self): # make sure that if we use `can_trade` at midnight, we don't pretend # we're in the previous day's last minute - the_day_after = self.trading_schedule.next_execution_day( + the_day_after = self.trading_calendar.next_session_label( self.equity_minute_bar_days[-1] ) @@ -609,7 +612,7 @@ class TestDailyBarData(WithBarDataChecks, def make_equity_daily_bar_data(cls): for sid in cls.sids: yield sid, create_daily_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, cls.equity_daily_bar_days[0], cls.equity_daily_bar_days[-1], interval=2 - sid % 2 @@ -642,8 +645,8 @@ class TestDailyBarData(WithBarDataChecks, cls.ASSETS = [cls.ASSET1, cls.ASSET2] def test_day_before_assets_trading(self): - # use the day before self.equity_daily_bar_days[0] - day = self.trading_schedule.previous_execution_day( + # use the day before self.bcolz_daily_bar_days[0] + day = self.trading_calendar.previous_session_label( self.equity_daily_bar_days[0] ) @@ -748,7 +751,7 @@ class TestDailyBarData(WithBarDataChecks, def test_after_assets_dead(self): # both assets end on self.day[-1], so let's try the next day - next_day = self.trading_schedule.next_execution_day( + next_day = self.trading_calendar.next_session_label( self.equity_daily_bar_days[-1] ) diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py index e19fb36f..b80a2e9e 100644 --- a/tests/test_benchmark.py +++ b/tests/test_benchmark.py @@ -30,12 +30,12 @@ from zipline.testing import ( from zipline.testing.fixtures import ( WithDataPortal, WithSimParams, - WithTradingSchedule, + WithTradingCalendar, ZiplineTestCase, ) -class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, +class TestBenchmark(WithDataPortal, WithSimParams, WithTradingCalendar, ZiplineTestCase): START_DATE = pd.Timestamp('2006-01-03', tz='utc') END_DATE = pd.Timestamp('2006-12-29', tz='utc') @@ -70,9 +70,9 @@ class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, @classmethod def make_stock_dividends_data(cls): - declared_date = cls.sim_params.trading_days[45] - ex_date = cls.sim_params.trading_days[50] - record_date = pay_date = cls.sim_params.trading_days[55] + declared_date = cls.sim_params.sessions[45] + ex_date = cls.sim_params.sessions[50] + record_date = pay_date = cls.sim_params.sessions[55] return pd.DataFrame({ 'sid': np.array([4], dtype=np.uint32), 'payment_sid': np.array([5], dtype=np.uint32), @@ -84,10 +84,10 @@ class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, }) def test_normal(self): - days_to_use = self.sim_params.trading_days[1:] + days_to_use = self.sim_params.sessions[1:] source = BenchmarkSource( - 1, self.env, self.trading_schedule, days_to_use, self.data_portal + 1, self.env, self.trading_calendar, days_to_use, self.data_portal ) # should be the equivalent of getting the price history, then doing @@ -113,14 +113,14 @@ class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, BenchmarkSource( 3, self.env, - self.trading_schedule, - self.sim_params.trading_days[1:], + self.trading_calendar, + self.sim_params.sessions[1:], self.data_portal ) self.assertEqual( '3 does not exist on %s. It started trading on %s.' % - (self.sim_params.trading_days[1], benchmark_start), + (self.sim_params.sessions[1], benchmark_start), exc.exception.message ) @@ -128,33 +128,33 @@ class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, BenchmarkSource( 3, self.env, - self.trading_schedule, - self.sim_params.trading_days[120:], + self.trading_calendar, + self.sim_params.sessions[120:], self.data_portal ) self.assertEqual( '3 does not exist on %s. It stopped trading on %s.' % - (self.sim_params.trading_days[-1], benchmark_end), + (self.sim_params.sessions[-1], benchmark_end), exc2.exception.message ) def test_asset_IPOed_same_day(self): # gotta get some minute data up in here. # add sid 4 for a couple of days - minutes = self.trading_schedule.execution_minutes_for_days_in_range( - self.sim_params.trading_days[0], - self.sim_params.trading_days[5] + minutes = self.trading_calendar.minutes_for_sessions_in_range( + self.sim_params.sessions[0], + self.sim_params.sessions[5] ) tmp_reader = tmp_bcolz_equity_minute_bar_reader( - self.trading_schedule, - self.trading_schedule.all_execution_days, + self.trading_calendar, + self.trading_calendar.all_sessions, create_minute_bar_data(minutes, [2]), ) with tmp_reader as reader: data_portal = DataPortal( - self.env.asset_finder, self.trading_schedule, + self.env.asset_finder, self.trading_calendar, first_trading_day=reader.first_trading_day, equity_minute_reader=reader, equity_daily_reader=self.bcolz_equity_daily_bar_reader, @@ -164,12 +164,12 @@ class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, source = BenchmarkSource( 2, self.env, - self.trading_schedule, - self.sim_params.trading_days, + self.trading_calendar, + self.sim_params.sessions, data_portal ) - days_to_use = self.sim_params.trading_days + days_to_use = self.sim_params.sessions # first value should be 0.0, coming from daily data self.assertAlmostEquals(0.0, source.get_value(days_to_use[0])) @@ -193,8 +193,8 @@ class TestBenchmark(WithDataPortal, WithSimParams, WithTradingSchedule, with self.assertRaises(InvalidBenchmarkAsset) as exc: BenchmarkSource( - 4, self.env, self.trading_schedule, - self.sim_params.trading_days, self.data_portal + 4, self.env, self.trading_calendar, + self.sim_params.sessions, self.data_portal ) self.assertEqual("4 cannot be used as the benchmark because it has a " diff --git a/tests/test_blotter.py b/tests/test_blotter.py index e17c384f..e27df4dd 100644 --- a/tests/test_blotter.py +++ b/tests/test_blotter.py @@ -58,7 +58,7 @@ class BlotterTestCase(WithLogger, 'close': [50, 50], 'volume': [100, 400], }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) yield 25, pd.DataFrame( { @@ -68,7 +68,7 @@ class BlotterTestCase(WithLogger, 'close': [50, 50], 'volume': [100, 400], }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) @parameterized.expand([(MarketOrder(), None, None), @@ -218,10 +218,10 @@ class BlotterTestCase(WithLogger, blotter.slippage_func = FixedSlippage() filled_id = blotter.order(asset_24, 100, MarketOrder()) filled_order = None - blotter.current_dt = self.sim_params.trading_days[-1] + blotter.current_dt = self.sim_params.sessions[-1] bar_data = BarData( self.data_portal, - lambda: self.sim_params.trading_days[-1], + lambda: self.sim_params.sessions[-1], self.sim_params.data_frequency, ) txns, _, closed_orders = blotter.get_transactions(bar_data) @@ -270,8 +270,8 @@ class BlotterTestCase(WithLogger, self.assertEqual(cancelled_order.id, held_order.id) self.assertEqual(cancelled_order.status, ORDER_STATUS.CANCELLED) - for data in ([100, self.sim_params.trading_days[0]], - [400, self.sim_params.trading_days[1]]): + for data in ([100, self.sim_params.sessions[0]], + [400, self.sim_params.sessions[1]]): # Verify that incoming fills will change the order status. trade_amt = data[0] dt = data[1] diff --git a/tests/test_commissions.py b/tests/test_commissions.py index add4c7f9..dacfe562 100644 --- a/tests/test_commissions.py +++ b/tests/test_commissions.py @@ -131,7 +131,7 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase): @classmethod def make_equity_daily_bar_data(cls): - num_days = len(cls.sim_params.trading_days) + num_days = len(cls.sim_params.sessions) return trades_by_sid_to_dfs( { @@ -141,10 +141,10 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase): [100.0] * num_days, timedelta(days=1), cls.sim_params, - trading_schedule=cls.trading_schedule, + trading_calendar=cls.trading_calendar, ), }, - index=cls.sim_params.trading_days, + index=cls.sim_params.sessions, ) def get_results(self, algo_code): diff --git a/tests/test_data_portal.py b/tests/test_data_portal.py index 4adc6b29..7cffd7c1 100644 --- a/tests/test_data_portal.py +++ b/tests/test_data_portal.py @@ -27,7 +27,7 @@ class TestDataPortal(WithTradingEnvironment, ZiplineTestCase): super(TestDataPortal, self).init_instance_fixtures() self.data_portal = DataPortal(self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, first_trading_day=None) def test_bar_count_for_simple_transforms(self): @@ -42,7 +42,7 @@ class TestDataPortal(WithTradingEnvironment, ZiplineTestCase): # half an hour into july 9, getting a 4-"day" window should get us # all the minutes of 7/6, 7/7, 7/8, and 31 minutes of 7/9 - july_9_dt = self.trading_schedule.start_and_end( + july_9_dt = self.trading_calendar.open_and_close_for_session( pd.Timestamp("2015-07-09", tz='UTC') )[0] + Timedelta("30 minutes") @@ -65,7 +65,7 @@ class TestDataPortal(WithTradingEnvironment, ZiplineTestCase): # half an hour into nov 30, getting a 4-"day" window should get us # all the minutes of 11/24, 11/25, 11/27 (half day!), and 31 minutes # of 11/30 - nov_30_dt = self.trading_schedule.start_and_end( + nov_30_dt = self.trading_calendar.open_and_close_for_session( pd.Timestamp("2015-11-30", tz='UTC') )[0] + Timedelta("30 minutes") diff --git a/tests/test_exchange_calendar.py b/tests/test_exchange_calendar.py deleted file mode 100644 index 6c9ae0ad..00000000 --- a/tests/test_exchange_calendar.py +++ /dev/null @@ -1,341 +0,0 @@ -# -# Copyright 2016 Quantopian, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os.path import ( - abspath, - dirname, - join, -) -from unittest import TestCase -from collections import namedtuple - -import pandas as pd -import pytz -from pandas import ( - read_csv, - datetime, - Timestamp, - Timedelta, - date_range, -) -from pandas.util.testing import assert_frame_equal - -from zipline.errors import ( - CalendarNameCollision, - InvalidCalendarName, -) -from zipline.utils.calendars.exchange_calendar_nyse import NYSEExchangeCalendar -from zipline.utils.calendars.exchange_calendar import( - register_calendar, - deregister_calendar, - get_calendar, - clear_calendars, -) - - -class CalendarRegistrationTestCase(TestCase): - - def setUp(self): - self.dummy_cal_type = namedtuple('DummyCal', ('name')) - - def tearDown(self): - clear_calendars() - - def test_register_calendar(self): - # Build a fake calendar - dummy_cal = self.dummy_cal_type('DMY') - - # Try to register and retrieve the calendar - register_calendar(dummy_cal) - retr_cal = get_calendar('DMY') - self.assertEqual(dummy_cal, retr_cal) - - # Try to register again, expecting a name collision - with self.assertRaises(CalendarNameCollision): - register_calendar(dummy_cal) - - # Deregister the calendar and ensure that it is removed - deregister_calendar('DMY') - with self.assertRaises(InvalidCalendarName): - get_calendar('DMY') - - def test_force_registration(self): - dummy_nyse = self.dummy_cal_type('NYSE') - - # Get the actual NYSE calendar - real_nyse = get_calendar('NYSE') - - # Force a registration of the dummy NYSE - register_calendar(dummy_nyse, force=True) - - # Ensure that the dummy overwrote the real calendar - retr_cal = get_calendar('NYSE') - self.assertNotEqual(real_nyse, retr_cal) - - -class ExchangeCalendarTestBase(object): - - # Override in subclasses. - answer_key_filename = None - calendar_class = None - - @staticmethod - def load_answer_key(filename): - """ - Load a CSV from tests/resources/calendars/{filename}.csv - """ - fullpath = join( - dirname(abspath(__file__)), - 'resources', - 'calendars', - filename + '.csv', - ) - return read_csv( - fullpath, - index_col=0, - # NOTE: Merely passing parse_dates=True doesn't cause pandas to set - # the dtype correctly, and passing all reasonable inputs to the - # dtype kwarg cause read_csv to barf. - parse_dates=[0, 1, 2], - ).tz_localize('UTC') - - @classmethod - def setupClass(cls): - cls.answers = cls.load_answer_key(cls.answer_key_filename) - cls.start_date = cls.answers.index[0] - cls.end_date = cls.answers.index[-1] - cls.calendar = cls.calendar_class(cls.start_date, cls.end_date) - - def test_calculated_against_csv(self): - assert_frame_equal(self.calendar.schedule, self.answers) - - def test_is_open_on_minute(self): - for market_minute in self.answers.market_open: - market_minute_utc = market_minute.tz_localize('UTC') - # The exchange should be classified as open on its first minute - self.assertTrue( - self.calendar.is_open_on_minute(market_minute_utc) - ) - # Decrement minute by one, to minute where the market was not open - pre_market = market_minute_utc - pd.Timedelta(minutes=1) - self.assertFalse( - self.calendar.is_open_on_minute(pre_market) - ) - - def test_open_and_close(self): - for index, row in self.answers.iterrows(): - o_and_c = self.calendar.open_and_close(index) - self.assertEqual(o_and_c[0], - row['market_open'].tz_localize('UTC')) - self.assertEqual(o_and_c[1], - row['market_close'].tz_localize('UTC')) - - def test_no_nones_from_open_and_close(self): - """ - Ensures that, for all minutes in a week, the open_and_close method - never returns a tuple of Nones. - """ - start_week = Timestamp('11/18/2012 12:00AM', tz='EST') - end_week = start_week + Timedelta(days=7) - minutes_in_week = date_range(start_week, end_week, freq='Min') - - for dt in minutes_in_week: - open, close = self.calendar.open_and_close(dt) - self.assertIsNotNone(open, "Open value is None") - self.assertIsNotNone(close, "Close value is None") - - -class NYSECalendarTestCase(ExchangeCalendarTestBase, TestCase): - - answer_key_filename = 'nyse' - calendar_class = NYSEExchangeCalendar - - def test_newyears(self): - """ - Check whether the ExchangeCalendar contains certain dates. - """ - # January 2012 - # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 6 7 - # 8 9 10 11 12 13 14 - # 15 16 17 18 19 20 21 - # 22 23 24 25 26 27 28 - # 29 30 31 - - start_dt = Timestamp('1/1/12', tz='UTC') - end_dt = Timestamp('12/31/13', tz='UTC') - trading_days = self.calendar.trading_days(start=start_dt, end=end_dt) - - day_after_new_years_sunday = datetime( - 2012, 1, 2, tzinfo=pytz.utc) - - self.assertNotIn(day_after_new_years_sunday, - trading_days.index, - """ - If NYE falls on a weekend, {0} the Monday after is a holiday. - """.strip().format(day_after_new_years_sunday) - ) - - first_trading_day_after_new_years_sunday = datetime( - 2012, 1, 3, tzinfo=pytz.utc) - - self.assertIn(first_trading_day_after_new_years_sunday, - trading_days.index, - """ - If NYE falls on a weekend, {0} the Tuesday after is the first trading day. - """.strip().format(first_trading_day_after_new_years_sunday) - ) - - # January 2013 - # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 - # 6 7 8 9 10 11 12 - # 13 14 15 16 17 18 19 - # 20 21 22 23 24 25 26 - # 27 28 29 30 31 - - new_years_day = datetime( - 2013, 1, 1, tzinfo=pytz.utc) - - self.assertNotIn(new_years_day, - trading_days.index, - """ - If NYE falls during the week, e.g. {0}, it is a holiday. - """.strip().format(new_years_day) - ) - - first_trading_day_after_new_years = datetime( - 2013, 1, 2, tzinfo=pytz.utc) - - self.assertIn(first_trading_day_after_new_years, - trading_days.index, - """ - If the day after NYE falls during the week, {0} \ - is the first trading day. - """.strip().format(first_trading_day_after_new_years) - ) - - def test_thanksgiving(self): - """ - Check ExchangeCalendar Thanksgiving dates. - """ - # November 2005 - # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 - # 6 7 8 9 10 11 12 - # 13 14 15 16 17 18 19 - # 20 21 22 23 24 25 26 - # 27 28 29 30 - - start_dt = Timestamp('1/1/05', tz='UTC') - end_dt = Timestamp('12/31/12', tz='UTC') - trading_days = self.calendar.trading_days(start=start_dt, - end=end_dt) - - thanksgiving_with_four_weeks = datetime( - 2005, 11, 24, tzinfo=pytz.utc) - - self.assertNotIn(thanksgiving_with_four_weeks, - trading_days.index, - """ - If Nov has 4 Thursdays, {0} Thanksgiving is the last Thursady. - """.strip().format(thanksgiving_with_four_weeks) - ) - - # November 2006 - # Su Mo Tu We Th Fr Sa - # 1 2 3 4 - # 5 6 7 8 9 10 11 - # 12 13 14 15 16 17 18 - # 19 20 21 22 23 24 25 - # 26 27 28 29 30 - thanksgiving_with_five_weeks = datetime( - 2006, 11, 23, tzinfo=pytz.utc) - - self.assertNotIn(thanksgiving_with_five_weeks, - trading_days.index, - """ - If Nov has 5 Thursdays, {0} Thanksgiving is not the last week. - """.strip().format(thanksgiving_with_five_weeks) - ) - - first_trading_day_after_new_years_sunday = datetime( - 2012, 1, 3, tzinfo=pytz.utc) - - self.assertIn(first_trading_day_after_new_years_sunday, - trading_days.index, - """ - If NYE falls on a weekend, {0} the Tuesday after is the first trading day. - """.strip().format(first_trading_day_after_new_years_sunday) - ) - - def test_day_after_thanksgiving(self): - # November 2012 - # Su Mo Tu We Th Fr Sa - # 1 2 3 - # 4 5 6 7 8 9 10 - # 11 12 13 14 15 16 17 - # 18 19 20 21 22 23 24 - # 25 26 27 28 29 30 - fourth_friday_open = Timestamp('11/23/2012 11:00AM', tz='EST') - fourth_friday = Timestamp('11/23/2012 3:00PM', tz='EST') - self.assertTrue(self.calendar.is_open_on_minute(fourth_friday_open)) - self.assertFalse(self.calendar.is_open_on_minute(fourth_friday)) - - # November 2013 - # Su Mo Tu We Th Fr Sa - # 1 2 - # 3 4 5 6 7 8 9 - # 10 11 12 13 14 15 16 - # 17 18 19 20 21 22 23 - # 24 25 26 27 28 29 30 - fifth_friday_open = Timestamp('11/29/2013 11:00AM', tz='EST') - fifth_friday = Timestamp('11/29/2013 3:00PM', tz='EST') - self.assertTrue(self.calendar.is_open_on_minute(fifth_friday_open)) - self.assertFalse(self.calendar.is_open_on_minute(fifth_friday)) - - def test_early_close_independence_day_thursday(self): - """ - Until 2013, the market closed early the Friday after an - Independence Day on Thursday. Since then, the early close is on - Wednesday. - """ - # July 2002 - # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 6 - # 7 8 9 10 11 12 13 - # 14 15 16 17 18 19 20 - # 21 22 23 24 25 26 27 - # 28 29 30 31 - wednesday_before = Timestamp('7/3/2002 3:00PM', tz='EST') - friday_after_open = Timestamp('7/5/2002 11:00AM', tz='EST') - friday_after = Timestamp('7/5/2002 3:00PM', tz='EST') - self.assertTrue(self.calendar.is_open_on_minute(wednesday_before)) - self.assertTrue(self.calendar.is_open_on_minute(friday_after_open)) - self.assertFalse(self.calendar.is_open_on_minute(friday_after)) - - # July 2013 - # Su Mo Tu We Th Fr Sa - # 1 2 3 4 5 6 - # 7 8 9 10 11 12 13 - # 14 15 16 17 18 19 20 - # 21 22 23 24 25 26 27 - # 28 29 30 31 - wednesday_before = Timestamp('7/3/2013 3:00PM', tz='EST') - friday_after_open = Timestamp('7/5/2013 11:00AM', tz='EST') - friday_after = Timestamp('7/5/2013 3:00PM', tz='EST') - self.assertFalse(self.calendar.is_open_on_minute(wednesday_before)) - self.assertTrue(self.calendar.is_open_on_minute(friday_after_open)) - self.assertTrue(self.calendar.is_open_on_minute(friday_after)) diff --git a/tests/test_fetcher.py b/tests/test_fetcher.py index 5a1101ed..05760e8b 100644 --- a/tests/test_fetcher.py +++ b/tests/test_fetcher.py @@ -109,7 +109,7 @@ class FetcherTestCase(WithResponses, ) results = test_algo.run(FetcherDataPortal(self.env, - self.trading_schedule)) + self.trading_calendar)) return results @@ -143,7 +143,7 @@ def handle_data(context, data): # the minutely emission packets here. TradingAlgorithm.run() only # returns daily packets. test_algo.data_portal = FetcherDataPortal(self.env, - self.trading_schedule) + self.trading_calendar) gen = test_algo.get_generator() perf_packets = list(gen) diff --git a/tests/test_finance.py b/tests/test_finance.py index 2aa93070..7f307994 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -198,7 +198,7 @@ class FinanceTestCase(WithLogger, data_frequency="minute" ) - minutes = self.trading_schedule.execution_minute_window( + minutes = self.trading_calendar.minutes_window( sim_params.first_open, int((trade_interval.total_seconds() / 60) * trade_count) + 100) @@ -216,9 +216,15 @@ class FinanceTestCase(WithLogger, } write_bcolz_minute_data( - self.trading_schedule, - self.trading_schedule.execution_days_in_range(minutes[0], - minutes[-1]), + self.trading_calendar, + self.trading_calendar.sessions_in_range( + self.trading_calendar.minute_to_session_label( + minutes[0] + ), + self.trading_calendar.minute_to_session_label( + minutes[-1] + ) + ), tempdir.path, iteritems(assets), ) @@ -226,7 +232,7 @@ class FinanceTestCase(WithLogger, equity_minute_reader = BcolzMinuteBarReader(tempdir.path) data_portal = DataPortal( - env.asset_finder, self.trading_schedule, + env.asset_finder, self.trading_calendar, first_trading_day=equity_minute_reader.first_trading_day, equity_minute_reader=equity_minute_reader, ) @@ -235,7 +241,7 @@ class FinanceTestCase(WithLogger, data_frequency="daily" ) - days = sim_params.trading_days + days = sim_params.sessions assets = { 1: pd.DataFrame({ @@ -249,12 +255,14 @@ class FinanceTestCase(WithLogger, } path = os.path.join(tempdir.path, "testdata.bcolz") - BcolzDailyBarWriter(path, days).write(assets.items()) + BcolzDailyBarWriter(path, days, self.trading_calendar).write( + assets.items() + ) equity_daily_reader = BcolzDailyBarReader(path) data_portal = DataPortal( - env.asset_finder, self.trading_schedule, + env.asset_finder, self.trading_calendar, first_trading_day=equity_daily_reader.first_trading_day, equity_daily_reader=equity_daily_reader, ) @@ -275,7 +283,7 @@ class FinanceTestCase(WithLogger, else: alternator = 1 - tracker = PerformanceTracker(sim_params, self.trading_schedule, + tracker = PerformanceTracker(sim_params, self.trading_calendar, self.env) # replicate what tradesim does by going through every minute or day @@ -391,10 +399,10 @@ class TradingEnvironmentTestCase(WithLogger, """ def test_simulation_parameters(self): sp = SimulationParameters( - period_start=datetime(2008, 1, 1, tzinfo=pytz.utc), - period_end=datetime(2008, 12, 31, tzinfo=pytz.utc), + start_session=pd.Timestamp("2008-01-01", tz='UTC'), + end_session=pd.Timestamp("2008-12-31", tz='UTC'), capital_base=100000, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) self.assertTrue(sp.last_close.month == 12) @@ -412,10 +420,10 @@ class TradingEnvironmentTestCase(WithLogger, # 27 28 29 30 31 params = SimulationParameters( - period_start=datetime(2007, 12, 31, tzinfo=pytz.utc), - period_end=datetime(2008, 1, 7, tzinfo=pytz.utc), + start_session=pd.Timestamp("2007-12-31", tz='UTC'), + end_session=pd.Timestamp("2008-01-07", tz='UTC'), capital_base=100000, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) expected_trading_days = ( @@ -431,6 +439,9 @@ class TradingEnvironmentTestCase(WithLogger, ) num_expected_trading_days = 5 - self.assertEquals(num_expected_trading_days, params.days_in_period) + self.assertEquals( + num_expected_trading_days, + len(params.sessions) + ) np.testing.assert_array_equal(expected_trading_days, - params.trading_days.tolist()) + params.sessions.tolist()) diff --git a/tests/test_history.py b/tests/test_history.py index 290e9c5b..df020e25 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -79,9 +79,9 @@ class WithHistory(WithDataPortal): @classmethod def init_class_fixtures(cls): super(WithHistory, cls).init_class_fixtures() - cls.trading_days = cls.trading_schedule.execution_days_in_range( - start=cls.TRADING_START_DT, - end=cls.TRADING_END_DT + cls.trading_days = cls.trading_calendar.sessions_in_range( + cls.TRADING_START_DT, + cls.TRADING_END_DT ) cls.ASSET1 = cls.asset_finder.retrieve_asset(1) @@ -457,24 +457,24 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): for sid in sids: asset = cls.asset_finder.retrieve_asset(sid) data[sid] = create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, asset.start_date, asset.end_date, start_val=2, ) data[1] = create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, pd.Timestamp('2014-01-03', tz='utc'), - pd.Timestamp('2016-01-30', tz='utc'), + pd.Timestamp('2016-01-29', tz='utc'), start_val=2, ) asset2 = cls.asset_finder.retrieve_asset(2) data[asset2.sid] = create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, asset2.start_date, - cls.trading_schedule.previous_execution_day(asset2.end_date), + cls.trading_calendar.previous_session_label(asset2.end_date), start_val=2, minute_blacklist=[ pd.Timestamp('2015-01-08 14:31', tz='UTC'), @@ -489,29 +489,29 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # the thousands place. data[cls.MERGER_ASSET_SID] = data[cls.SPLIT_ASSET_SID] = pd.concat(( create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, pd.Timestamp('2015-01-05', tz='UTC'), pd.Timestamp('2015-01-05', tz='UTC'), start_val=8000), create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, pd.Timestamp('2015-01-06', tz='UTC'), pd.Timestamp('2015-01-06', tz='UTC'), start_val=2000), create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, pd.Timestamp('2015-01-07', tz='UTC'), pd.Timestamp('2015-01-07', tz='UTC'), start_val=1000), create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, pd.Timestamp('2015-01-08', tz='UTC'), pd.Timestamp('2015-01-08', tz='UTC'), start_val=1000) )) asset3 = cls.asset_finder.retrieve_asset(3) data[3] = create_minute_df_for_asset( - cls.trading_schedule, + cls.trading_calendar, asset3.start_date, asset3.end_date, start_val=2, @@ -536,12 +536,12 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): end = pd.Timestamp('2014-04-10', tz='UTC') sim_params = SimulationParameters( - period_start=start, - period_end=end, + start_session=start, + end_session=end, capital_base=float('1.0e5'), data_frequency='minute', emission_rate='daily', - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) test_algo = TradingAlgorithm( @@ -564,7 +564,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # before any of the adjustments, 1/4 and 1/5 window1 = self.data_portal.get_history_window( [asset], - self.trading_schedule.start_and_end(jan5)[1], + self.trading_calendar.open_and_close_for_session(jan5)[1], 2, '1d', 'close' @@ -625,7 +625,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # before any of the dividends window1 = self.data_portal.get_history_window( [asset], - self.trading_schedule.start_and_end(jan5)[1], + self.trading_calendar.open_and_close_for_session(jan5)[1], 2, '1d', 'close' @@ -680,8 +680,8 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_minute_before_assets_trading(self): # since asset2 and asset3 both started trading on 1/5/2015, let's do # some history windows that are completely before that - minutes = self.trading_schedule.execution_minutes_for_day( - self.trading_schedule.previous_execution_day(pd.Timestamp( + minutes = self.trading_calendar.minutes_for_session( + self.trading_calendar.previous_session_label(pd.Timestamp( '2015-01-05', tz='UTC' )) )[0:60] @@ -730,7 +730,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # 10 minutes asset = self.env.asset_finder.retrieve_asset(sid) - minutes = self.trading_schedule.execution_minutes_for_day( + minutes = self.trading_calendar.minutes_for_session( pd.Timestamp('2015-01-05', tz='UTC') )[0:60] @@ -741,8 +741,11 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_minute_midnight(self): midnight = pd.Timestamp('2015-01-06', tz='UTC') - last_minute = self.trading_schedule.start_and_end( - self.trading_schedule.previous_execution_day(midnight) + last_minute = self.trading_calendar.open_and_close_for_session( + self.trading_calendar.minute_to_session_label( + midnight, + direction="previous" + ) )[1] midnight_bar_data = \ @@ -761,7 +764,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_minute_after_asset_stopped(self): # SHORT_ASSET's last day was 2015-01-06 # get some history windows that straddle the end - minutes = self.trading_schedule.execution_minutes_for_day( + minutes = self.trading_calendar.minutes_for_session( pd.Timestamp('2015-01-07', tz='UTC') )[0:60] @@ -856,7 +859,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # before any of the adjustments, last 10 minutes of jan 5 window1 = self.data_portal.get_history_window( [asset], - self.trading_schedule.start_and_end(jan5)[1], + self.trading_calendar.open_and_close_for_session(jan5)[1], 10, '1m', 'close' @@ -1105,21 +1108,21 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_minute_different_lifetimes(self): # at trading start, only asset1 existed - day = self.trading_schedule.next_execution_day(self.TRADING_START_DT) + day = self.trading_calendar.next_session_label(self.TRADING_START_DT) asset1_minutes = \ - self.trading_schedule.execution_minutes_for_days_in_range( - start=self.ASSET1.start_date, - end=self.ASSET1.end_date + self.trading_calendar.minutes_for_sessions_in_range( + self.ASSET1.start_date, + self.ASSET1.end_date ) asset1_idx = asset1_minutes.searchsorted( - self.trading_schedule.start_and_end(day)[0] + self.trading_calendar.open_and_close_for_session(day)[0] ) window = self.data_portal.get_history_window( [self.ASSET1, self.ASSET2], - self.trading_schedule.start_and_end(day)[0], + self.trading_calendar.open_and_close_for_session(day)[0], 100, '1m', 'close' @@ -1137,7 +1140,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_history_window_before_first_trading_day(self): # trading_start is 2/3/2014 # get a history window that starts before that, and ends after that - first_day_minutes = self.trading_schedule.execution_minutes_for_day( + first_day_minutes = self.trading_calendar.minutes_for_session( self.TRADING_START_DT ) exp_msg = ( @@ -1157,7 +1160,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # January 2015 has both daily and minute data for ASSET2 day = pd.Timestamp('2015-01-07', tz='UTC') - minutes = self.trading_schedule.execution_minutes_for_day(day) + minutes = self.trading_calendar.minutes_for_session(day) # minute data, baseline: # Jan 5: 2 to 391 @@ -1221,7 +1224,7 @@ class MinuteEquityHistoryTestCase(WithHistory, ZiplineTestCase): # January 2015 has both daily and minute data for ASSET2 day = pd.Timestamp('2015-01-08', tz='UTC') - minutes = self.trading_schedule.execution_minutes_for_day(day) + minutes = self.trading_calendar.minutes_for_session(day) # minute data, baseline: # Jan 5: 2 to 391 @@ -1340,28 +1343,27 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): @classmethod def create_df_for_asset(cls, start_day, end_day, interval=1, force_zeroes=False): - days = cls.trading_schedule.execution_days_in_range(start_day, - end_day) - days_count = len(days) + sessions = cls.trading_calendar.sessions_in_range(start_day, end_day) + sessions_count = len(sessions) # default to 2 because the low array subtracts 1, and we don't # want to start with a 0 - days_arr = np.array(range(2, days_count + 2)) + sessions_arr = np.array(range(2, sessions_count + 2)) df = pd.DataFrame( { - 'open': days_arr + 1, - 'high': days_arr + 2, - 'low': days_arr - 1, - 'close': days_arr, - 'volume': 100 * days_arr, + 'open': sessions_arr + 1, + 'high': sessions_arr + 2, + 'low': sessions_arr - 1, + 'close': sessions_arr, + 'volume': 100 * sessions_arr, }, - index=days, + index=sessions, ) if interval > 1: counter = 0 - while counter < days_count: + while counter < sessions_count: df[counter:(counter + interval - 1)] = 0 counter += interval @@ -1370,9 +1372,9 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_daily_before_assets_trading(self): # asset2 and asset3 both started trading in 2015 - days = self.trading_schedule.execution_days_in_range( - start=pd.Timestamp('2014-12-15', tz='UTC'), - end=pd.Timestamp('2014-12-18', tz='UTC'), + days = self.trading_calendar.sessions_in_range( + pd.Timestamp('2014-12-15', tz='UTC'), + pd.Timestamp('2014-12-18', tz='UTC'), ) for idx, day in enumerate(days): @@ -1406,12 +1408,9 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): # 10 days # get the first 30 days of 2015 - jan5 = pd.Timestamp('2015-01-04') + jan5 = pd.Timestamp('2015-01-05') - days = self.trading_schedule.execution_days_in_range( - start=jan5, - end=self.trading_schedule.add_execution_days(30, jan5) - ) + days = self.trading_calendar.sessions_window(jan5, 30) for idx, day in enumerate(days): self.verify_regular_dt(idx, day, 'daily') @@ -1453,9 +1452,9 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_daily_after_asset_stopped(self): # SHORT_ASSET trades on 1/5, 1/6, that's it. - days = self.trading_schedule.execution_days_in_range( - start=pd.Timestamp('2015-01-07', tz='UTC'), - end=pd.Timestamp('2015-01-08', tz='UTC') + days = self.trading_calendar.sessions_in_range( + pd.Timestamp('2015-01-07', tz='UTC'), + pd.Timestamp('2015-01-08', tz='UTC') ) # days has 1/7, 1/8 @@ -1644,7 +1643,7 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): def test_history_window_before_first_trading_day(self): # trading_start is 2/3/2014 # get a history window that starts before that, and ends after that - second_day = self.trading_schedule.next_execution_day( + second_day = self.trading_calendar.next_session_label( self.TRADING_START_DT ) @@ -1673,7 +1672,7 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): # Use a minute to force minute mode. first_minute = \ - self.trading_schedule.schedule.market_open[self.TRADING_START_DT] + self.trading_calendar.schedule.market_open[self.TRADING_START_DT] with self.assertRaisesRegexp(HistoryWindowStartsBeforeData, exp_msg): self.data_portal.get_history_window( @@ -1803,7 +1802,7 @@ class MinuteToDailyAggregationTestCase(WithBcolzEquityMinuteBarReader, # Set up a fresh data portal for each test, since order of calling # needs to be tested. self.equity_daily_aggregator = DailyHistoryAggregator( - self.trading_schedule.schedule.market_open, + self.trading_calendar.schedule.market_open, self.bcolz_equity_minute_bar_reader, ) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 29755dca..d8d13820 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -57,10 +57,10 @@ from zipline.testing.fixtures import ( WithSimParams, WithTmpDir, WithTradingEnvironment, - WithTradingSchedule, + WithTradingCalendar, ZiplineTestCase, ) -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar logger = logging.getLogger('Test Perf Tracking') @@ -177,13 +177,13 @@ def calculate_results(sim_params, splits = splits or {} commissions = commissions or {} - perf_tracker = perf.PerformanceTracker(sim_params, - default_nyse_schedule, - env) + perf_tracker = perf.PerformanceTracker( + sim_params, get_calendar("NYSE"), env + ) results = [] - for date in sim_params.trading_days: + for date in sim_params.sessions: for txn in filter(lambda txn: txn.dt == date, txns): # Process txns for this date. perf_tracker.process_transaction(txn) @@ -216,7 +216,7 @@ def check_perf_tracker_serialization(perf_tracker): 'txn_count', 'market_open', 'last_close', - 'period_start', + 'start_session', 'day_count', 'capital_base', 'market_close', @@ -243,9 +243,9 @@ def setup_env_data(env, sim_params, sids, futures_sids=[]): data = {} for sid in sids: data[sid] = { - "start_date": sim_params.trading_days[0], - "end_date": default_nyse_schedule.next_execution_day( - sim_params.trading_days[-1] + "start_date": sim_params.sessions[0], + "end_date": get_calendar("NYSE").next_session_label( + sim_params.sessions[-1] ) } @@ -254,9 +254,10 @@ def setup_env_data(env, sim_params, sids, futures_sids=[]): futures_data = {} for future_sid in futures_sids: futures_data[future_sid] = { - "start_date": sim_params.trading_days[0], - "end_date": default_nyse_schedule.next_execution_day( - sim_params.trading_days[-1] + "start_date": sim_params.sessions[0], + # (obviously) FIXME once we have a future calendar + "end_date": get_calendar("NYSE").next_session_label( + sim_params.sessions[-1] ), "multiplier": 100 } @@ -280,7 +281,7 @@ class TestSplitPerformance(WithSimParams, WithTmpDir, ZiplineTestCase): # if multiple positions all have splits at the same time, verify that # the total leftover cash is correct perf_tracker = perf.PerformanceTracker(self.sim_params, - self.trading_schedule, + self.trading_calendar, self.env) asset1 = self.asset_finder.retrieve_asset(1) @@ -310,14 +311,14 @@ class TestSplitPerformance(WithSimParams, WithTmpDir, ZiplineTestCase): [100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) # set up a long position in sid 1 # 100 shares at $20 apiece = $2000 position data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.tmpdir, self.sim_params, {1: events}, @@ -422,7 +423,7 @@ class TestDividendPerformance(WithSimParams, after = factory.get_next_trading_dt( before, timedelta(days=1), - self.trading_schedule, + self.trading_calendar, ) self.assertEqual(after.hour, 13) @@ -434,7 +435,7 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -442,7 +443,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -457,7 +458,7 @@ class TestDividendPerformance(WithSimParams, adjustment_reader = SQLiteAdjustmentReader(dbpath) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -500,7 +501,7 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -508,7 +509,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -534,7 +535,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, events, @@ -575,7 +576,7 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -583,7 +584,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -599,7 +600,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -637,7 +638,7 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -645,7 +646,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -661,7 +662,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -693,6 +694,8 @@ class TestDividendPerformance(WithSimParams, [-1000, -1000, 0, 1000, 1000, 1000]) def test_buy_and_sell_before_ex(self): + # need a six-day simparam + # post some trades in the market events = factory.create_trade_history( self.asset1, @@ -700,14 +703,14 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() @@ -724,7 +727,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -761,21 +764,21 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) pay_date = self.sim_params.first_open # find pay date that is much later. for i in range(30): pay_date = factory.get_next_trading_dt(pay_date, oneday, - self.trading_schedule) + self.trading_calendar) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -791,7 +794,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -829,7 +832,7 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -837,7 +840,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -853,7 +856,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -888,7 +891,7 @@ class TestDividendPerformance(WithSimParams, [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -896,7 +899,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -912,7 +915,7 @@ class TestDividendPerformance(WithSimParams, data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: events}, @@ -941,11 +944,11 @@ class TestDividendPerformance(WithSimParams, # post some trades in the market events = factory.create_trade_history( self.asset1, - [10, 10, 10, 10, 10], - [100, 100, 100, 100, 100], + [10, 10, 10, 10, 10, 10], + [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) dbpath = self.instance_tmpdir.getpath('adjustments.sqlite') @@ -953,7 +956,7 @@ class TestDividendPerformance(WithSimParams, writer = SQLiteAdjustmentWriter( dbpath, MockDailyBarReader(), - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, ) splits = mergers = create_empty_splits_mergers_frame() dividends = pd.DataFrame({ @@ -963,7 +966,11 @@ class TestDividendPerformance(WithSimParams, 'ex_date': np.array([events[-2].dt], dtype='datetime64[ns]'), 'record_date': np.array([events[0].dt], dtype='datetime64[ns]'), 'pay_date': np.array( - [self.trading_schedule.next_execution_day(events[-1].dt)], + [self.trading_calendar.next_session_label( + self.trading_calendar.minute_to_session_label( + events[-1].dt + ) + )], dtype='datetime64[ns]'), }) writer.write(splits, mergers, dividends) @@ -973,16 +980,18 @@ class TestDividendPerformance(WithSimParams, sim_params = create_simulation_parameters( num_days=6, capital_base=10e3, - start=self.sim_params.period_start, - end=self.sim_params.period_end + start=self.sim_params.start_session, + end=self.sim_params.end_session ) - sim_params.period_end = events[-1].dt - sim_params.update_internal_from_trading_schedule(self.trading_schedule) + sim_params = sim_params.create_new( + sim_params.start_session, + events[-1].dt + ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, sim_params, {1: events}, @@ -997,18 +1006,18 @@ class TestDividendPerformance(WithSimParams, txns=txns, ) - self.assertEqual(len(results), 5) + self.assertEqual(len(results), 6) cumulative_returns = \ [event['cumulative_perf']['returns'] for event in results] - self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0]) + self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) daily_returns = [event['daily_perf']['returns'] for event in results] - self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0]) + self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) cash_flows = [event['daily_perf']['capital_used'] for event in results] - self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0]) + self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0, 0]) cumulative_cash_flows = \ [event['cumulative_perf']['capital_used'] for event in results] self.assertEqual(cumulative_cash_flows, - [-1000, -1000, -1000, -1000, -1000]) + [-1000, -1000, -1000, -1000, -1000, -1000]) class TestDividendPerformanceHolidayStyle(TestDividendPerformance): @@ -1022,7 +1031,7 @@ class TestDividendPerformanceHolidayStyle(TestDividendPerformance): END_DATE = pd.Timestamp('2003-12-08', tz='utc') -class TestPositionPerformance(WithInstanceTmpDir, WithTradingSchedule, +class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendar, ZiplineTestCase): def create_environment_stuff(self, num_days=4, @@ -1072,7 +1081,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingSchedule, [100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) trades_2 = factory.create_trade_history( @@ -1081,12 +1090,12 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingSchedule, [100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades_1, 2: trades_2} @@ -1178,12 +1187,12 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingSchedule, [100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -1270,12 +1279,12 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingSchedule, [100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -1284,8 +1293,8 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingSchedule, self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency, - period_open=self.sim_params.period_start, - period_close=self.sim_params.period_end) + period_open=self.sim_params.start_session, + period_close=self.sim_params.end_session) pp.position_tracker = pt pt.execute_transaction(txn) @@ -1386,14 +1395,14 @@ single short-sale transaction""" [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) trades_1 = trades[:-2] data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -1620,12 +1629,12 @@ cost of sole txn in test" [100, 100, 100, 100], oneday, sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {3: trades} @@ -1740,12 +1749,12 @@ single short-sale transaction""" [100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {3: trades} @@ -1985,12 +1994,12 @@ trade after cover""" [100, 100, 100, 100, 100, 100, 100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -2072,14 +2081,14 @@ shares in position" [100, 100, 100, 100, 100], oneday, self.sim_params, - self.trading_schedule, + self.trading_calendar, ) trades = factory.create_trade_history(*history_args) transactions = factory.create_txn_history(*history_args)[:4] data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -2090,8 +2099,8 @@ shares in position" 1000.0, self.env.asset_finder, self.sim_params.data_frequency, - period_open=self.sim_params.period_start, - period_close=self.sim_params.trading_days[-1] + period_open=self.sim_params.start_session, + period_close=self.sim_params.sessions[-1] ) pp.position_tracker = pt @@ -2198,7 +2207,7 @@ shares in position" [200, -100, -100, 100, -300, 100, 500, 400], oneday, self.sim_params, - self.trading_schedule, + self.trading_calendar, ) cost_bases = [10, 10, 0, 8, 9, 9, 13, 13.5] @@ -2234,12 +2243,12 @@ shares in position" [100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -2248,8 +2257,8 @@ shares in position" self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency, - period_open=self.sim_params.period_start, - period_close=self.sim_params.period_end) + period_open=self.sim_params.start_session, + period_close=self.sim_params.end_session) pp.position_tracker = pt pt.execute_transaction(txn) @@ -2279,12 +2288,12 @@ shares in position" [100, 100, 100, 100], oneday, self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) data_portal = create_data_portal_from_trade_history( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, self.instance_tmpdir, self.sim_params, {1: trades}) @@ -2293,8 +2302,8 @@ shares in position" self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency, - period_open=self.sim_params.period_start, - period_close=self.sim_params.period_end) + period_open=self.sim_params.start_session, + period_close=self.sim_params.end_session) pp.position_tracker = pt pt.execute_transaction(txn) diff --git a/tests/test_security_list.py b/tests/test_security_list.py index 9e96d81a..756e2210 100644 --- a/tests/test_security_list.py +++ b/tests/test_security_list.py @@ -14,7 +14,7 @@ from zipline.testing import ( ) from zipline.testing.fixtures import ( WithLogger, - WithTradingSchedule, + WithTradingCalendar, ZiplineTestCase, ) from zipline.utils import factory @@ -67,7 +67,7 @@ class IterateRLAlgo(TradingAlgorithm): self.found = True -class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): +class SecurityListTestCase(WithLogger, WithTradingCalendar, ZiplineTestCase): @classmethod def init_class_fixtures(cls): @@ -75,7 +75,7 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): # this is ugly, but we need to create two different # TradingEnvironment/DataPortal pairs - start = list(LEVERAGED_ETFS.keys())[0] + cls.start = pd.Timestamp(list(LEVERAGED_ETFS.keys())[0]) end = pd.Timestamp('2015-02-17', tz='utc') cls.extra_knowledge_date = pd.Timestamp('2015-01-27', tz='utc') cls.trading_day_before_first_kd = pd.Timestamp('2015-01-23', tz='utc') @@ -83,15 +83,16 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): cls.env = cls.enter_class_context(tmp_trading_env( equities=pd.DataFrame.from_records([{ - 'start_date': start, + 'start_date': cls.start, 'end_date': end, 'symbol': symbol } for symbol in symbols]), )) + cls.sim_params = factory.create_simulation_parameters( - start=start, + start=cls.start, num_days=4, - trading_schedule=cls.trading_schedule + trading_calendar=cls.trading_calendar ) cls.sim_params2 = sp2 = factory.create_simulation_parameters( @@ -100,8 +101,8 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): cls.env2 = cls.enter_class_context(tmp_trading_env( equities=pd.DataFrame.from_records([{ - 'start_date': sp2.period_start, - 'end_date': sp2.period_end, + 'start_date': sp2.start_session, + 'end_date': sp2.end_session, 'symbol': symbol } for symbol in symbols]), )) @@ -114,7 +115,7 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): tempdir=cls.tempdir, sim_params=cls.sim_params, sids=range(0, 5), - trading_schedule=cls.trading_schedule, + trading_calendar=cls.trading_calendar, ) cls.data_portal2 = create_data_portal( @@ -122,7 +123,7 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): tempdir=cls.tempdir2, sim_params=cls.sim_params2, sids=range(0, 5), - trading_schedule=cls.trading_schedule, + trading_calendar=cls.trading_calendar, ) def test_iterate_over_restricted_list(self): @@ -136,7 +137,7 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): # set the knowledge date to the first day of the # leveraged etf knowledge date. def get_datetime(): - return list(LEVERAGED_ETFS.keys())[0] + return self.start rl = SecurityListSet(get_datetime, self.env.asset_finder) # assert that a sample from the leveraged list are in restricted @@ -217,15 +218,16 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): def test_algo_with_rl_violation_after_knowledge_date(self): sim_params = factory.create_simulation_parameters( - start=list( - LEVERAGED_ETFS.keys())[0] + timedelta(days=7), num_days=5) + start=self.start + timedelta(days=7), + num_days=5 + ) data_portal = create_data_portal( self.env.asset_finder, self.tempdir, sim_params=sim_params, sids=range(0, 5), - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) algo = RestrictedAlgoWithoutCheck(symbol='BZQ', @@ -243,8 +245,9 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): set is still disallowed. """ sim_params = factory.create_simulation_parameters( - start=list( - LEVERAGED_ETFS.keys())[0] + timedelta(days=7), num_days=4) + start=self.start + timedelta(days=7), + num_days=4 + ) with security_list_copy(): add_security_data(['AAPL'], []) @@ -262,8 +265,8 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): ) equities = pd.DataFrame.from_records([{ 'symbol': 'BZQ', - 'start_date': sim_params.period_start, - 'end_date': sim_params.period_end, + 'start_date': sim_params.start_session, + 'end_date': sim_params.end_session, }]) with TempDirectory() as new_tempdir, \ security_list_copy(), \ @@ -277,7 +280,7 @@ class SecurityListTestCase(WithLogger, WithTradingSchedule, ZiplineTestCase): new_tempdir, sim_params, range(0, 5), - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) algo = RestrictedAlgoWithoutCheck( diff --git a/tests/test_tradesimulation.py b/tests/test_tradesimulation.py index f157ff72..87499cef 100644 --- a/tests/test_tradesimulation.py +++ b/tests/test_tradesimulation.py @@ -53,13 +53,13 @@ class TestTradeSimulation(TestCase): self.fake_minutely_benchmark): algo = NoopAlgorithm(sim_params=params) algo.run(FakeDataPortal()) - self.assertEqual(algo.perf_tracker.day_count, 1.0) + self.assertEqual(len(algo.perf_tracker.sim_params.sessions), 1) - @parameterized.expand([('%s_%s_%s' % (num_days, freq, emission_rate), - num_days, freq, emission_rate) + @parameterized.expand([('%s_%s_%s' % (num_sessions, freq, emission_rate), + num_sessions, freq, emission_rate) for freq in FREQUENCIES for emission_rate in FREQUENCIES - for num_days in range(1, 4) + for num_sessions in range(1, 4) if FREQUENCIES[emission_rate] <= FREQUENCIES[freq]]) def test_before_trading_start(self, test_name, num_days, freq, emission_rate): @@ -75,9 +75,10 @@ class TestTradeSimulation(TestCase): algo = BeforeTradingAlgorithm(sim_params=params) algo.run(FakeDataPortal()) - self.assertEqual(algo.perf_tracker.day_count, num_days) + self.assertEqual(len(algo.perf_tracker.sim_params.sessions), + num_days) - self.assertTrue(params.trading_days.equals( + self.assertTrue(params.sessions.equals( pd.DatetimeIndex(algo.before_trading_at)), "Expected %s but was %s." - % (params.trading_days, algo.before_trading_at)) + % (params.sessions, algo.before_trading_at)) diff --git a/tests/test_trading_calendar.py b/tests/test_trading_calendar.py new file mode 100644 index 00000000..f00feefd --- /dev/null +++ b/tests/test_trading_calendar.py @@ -0,0 +1,762 @@ +# +# Copyright 2016 Quantopian, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os.path import ( + abspath, + dirname, + join, +) +from unittest import TestCase +from collections import namedtuple + +import numpy as np +import pandas as pd +from pandas import ( + read_csv, + Timestamp, +) +from pandas.util.testing import assert_index_equal +from zipline.errors import ( + CalendarNameCollision, + InvalidCalendarName, +) +from zipline.utils.calendars.exchange_calendar_nyse import NYSEExchangeCalendar +from zipline.utils.calendars import( + register_calendar, + deregister_calendar, + get_calendar, + clear_calendars, +) + + +class CalendarRegistrationTestCase(TestCase): + + def setUp(self): + self.dummy_cal_type = namedtuple('DummyCal', ('name')) + + def tearDown(self): + clear_calendars() + + def test_register_calendar(self): + # Build a fake calendar + dummy_cal = self.dummy_cal_type('DMY') + + # Try to register and retrieve the calendar + register_calendar(dummy_cal) + retr_cal = get_calendar('DMY') + self.assertEqual(dummy_cal, retr_cal) + + # Try to register again, expecting a name collision + with self.assertRaises(CalendarNameCollision): + register_calendar(dummy_cal) + + # Deregister the calendar and ensure that it is removed + deregister_calendar('DMY') + with self.assertRaises(InvalidCalendarName): + get_calendar('DMY') + + def test_force_registration(self): + dummy_nyse = self.dummy_cal_type('NYSE') + + # Get the actual NYSE calendar + real_nyse = get_calendar('NYSE') + + # Force a registration of the dummy NYSE + register_calendar(dummy_nyse, force=True) + + # Ensure that the dummy overwrote the real calendar + retr_cal = get_calendar('NYSE') + self.assertNotEqual(real_nyse, retr_cal) + + +class ExchangeCalendarTestBase(object): + + # Override in subclasses. + answer_key_filename = None + calendar_class = None + + @staticmethod + def load_answer_key(filename): + """ + Load a CSV from tests/resources/calendars/{filename}.csv + """ + fullpath = join( + dirname(abspath(__file__)), + 'resources', + 'calendars', + filename + '.csv', + ) + + return read_csv( + fullpath, + index_col=0, + # NOTE: Merely passing parse_dates=True doesn't cause pandas to set + # the dtype correctly, and passing all reasonable inputs to the + # dtype kwarg cause read_csv to barf. + parse_dates=[0, 1, 2], + date_parser=lambda x: pd.Timestamp(x, tz='UTC') + ) + + @classmethod + def setupClass(cls): + cls.answers = cls.load_answer_key(cls.answer_key_filename) + + cls.start_date = cls.answers.index[0] + cls.end_date = cls.answers.index[-1] + cls.calendar = cls.calendar_class(cls.start_date, cls.end_date) + + cls.one_minute = pd.Timedelta(minutes=1) + cls.one_hour = pd.Timedelta(hours=1) + + def test_calculated_against_csv(self): + assert_index_equal(self.calendar.schedule.index, self.answers.index) + + def test_is_open_on_minute(self): + one_minute = pd.Timedelta(minutes=1) + + for market_minute in self.answers.market_open: + market_minute_utc = market_minute + # The exchange should be classified as open on its first minute + self.assertTrue(self.calendar.is_open_on_minute(market_minute_utc)) + + # Decrement minute by one, to minute where the market was not open + pre_market = market_minute_utc - one_minute + self.assertFalse(self.calendar.is_open_on_minute(pre_market)) + + for market_minute in self.answers.market_close: + close_minute_utc = market_minute + # should be open on its last minute + self.assertTrue(self.calendar.is_open_on_minute(close_minute_utc)) + + # increment minute by one minute, should be closed + post_market = close_minute_utc + one_minute + self.assertFalse(self.calendar.is_open_on_minute(post_market)) + + def _verify_minute(self, calendar, minute, + next_open_answer, prev_open_answer, + next_close_answer, prev_close_answer): + self.assertEqual( + calendar.next_open(minute), + next_open_answer + ) + + self.assertEqual( + self.calendar.previous_open(minute), + prev_open_answer + ) + + self.assertEqual( + self.calendar.next_close(minute), + next_close_answer + ) + + self.assertEqual( + self.calendar.previous_close(minute), + prev_close_answer + ) + + def test_next_prev_open_close(self): + # for each session, check: + # - the minute before the open + # - the first minute of the session + # - the second minute of the session + # - the minute before the close + # - the last minute of the session + # - the first minute after the close + answers_to_use = self.answers[1:-2] + + for idx, info in enumerate(answers_to_use.iterrows()): + open_minute = info[1].iloc[0] + close_minute = info[1].iloc[1] + + minute_before_open = open_minute - self.one_minute + + # answers_to_use starts at the second element of self.answers, + # so self.answers.iloc[idx] is one element before, and + # self.answers.iloc[idx + 2] is one element after the current + # element + previous_open = self.answers.iloc[idx].market_open + next_open = self.answers.iloc[idx + 2].market_open + previous_close = self.answers.iloc[idx].market_close + next_close = self.answers.iloc[idx + 2].market_close + + # minute before open + self._verify_minute( + self.calendar, minute_before_open, open_minute, previous_open, + close_minute, previous_close + ) + + # open minute + self._verify_minute( + self.calendar, open_minute, next_open, previous_open, + close_minute, previous_close + ) + + # second minute of session + self._verify_minute( + self.calendar, open_minute + self.one_minute, next_open, + open_minute, close_minute, previous_close + ) + + # minute before the close + self._verify_minute( + self.calendar, close_minute - self.one_minute, next_open, + open_minute, close_minute, previous_close + ) + + # the close + self._verify_minute( + self.calendar, close_minute, next_open, open_minute, + next_close, previous_close + ) + + # minute after the close + self._verify_minute( + self.calendar, close_minute + self.one_minute, next_open, + open_minute, next_close, close_minute + ) + + def test_next_prev_minute(self): + all_minutes = self.calendar.all_minutes + + # test 20,000 minutes because it takes too long to do the rest. + for idx, minute in enumerate(all_minutes[1:20000]): + self.assertEqual( + all_minutes[idx + 2], + self.calendar.next_minute(minute) + ) + + self.assertEqual( + all_minutes[idx], + self.calendar.previous_minute(minute) + ) + + # test a couple of non-market minutes + for open_minute in self.answers.market_open[1:]: + hour_before_open = open_minute - self.one_hour + self.assertEqual( + open_minute, + self.calendar.next_minute(hour_before_open) + ) + + for close_minute in self.answers.market_close[1:]: + hour_after_close = close_minute + self.one_hour + self.assertEqual( + close_minute, + self.calendar.previous_minute(hour_after_close) + ) + + def test_minute_to_session_label(self): + for idx, info in enumerate(self.answers[1:-2].iterrows()): + session_label = info[1].name + open_minute = info[1].iloc[0] + close_minute = info[1].iloc[1] + hour_into_session = open_minute + self.one_hour + + minute_before_session = open_minute - self.one_minute + minute_after_session = close_minute + self.one_minute + + next_session_label = self.answers.iloc[idx + 2].name + previous_session_label = self.answers.iloc[idx].name + + # verify that minutes inside a session resolve correctly + minutes_that_resolve_to_this_session = [ + self.calendar.minute_to_session_label(open_minute), + self.calendar.minute_to_session_label(open_minute, + direction="next"), + self.calendar.minute_to_session_label(open_minute, + direction="previous"), + self.calendar.minute_to_session_label(open_minute, + direction="none"), + self.calendar.minute_to_session_label(hour_into_session), + self.calendar.minute_to_session_label(hour_into_session, + direction="next"), + self.calendar.minute_to_session_label(hour_into_session, + direction="previous"), + self.calendar.minute_to_session_label(hour_into_session, + direction="none"), + self.calendar.minute_to_session_label(close_minute), + self.calendar.minute_to_session_label(close_minute, + direction="next"), + self.calendar.minute_to_session_label(close_minute, + direction="previous"), + self.calendar.minute_to_session_label(close_minute, + direction="none"), + self.calendar.minute_to_session_label(minute_before_session), + self.calendar.minute_to_session_label( + minute_before_session, + direction="next" + ), + self.calendar.minute_to_session_label( + minute_after_session, + direction="previous" + ), + session_label + ] + + self.assertTrue(all(x == minutes_that_resolve_to_this_session[0] + for x in minutes_that_resolve_to_this_session)) + + minutes_that_resolve_to_next_session = [ + self.calendar.minute_to_session_label(minute_after_session), + self.calendar.minute_to_session_label(minute_after_session, + direction="next"), + next_session_label + ] + + self.assertTrue(all(x == minutes_that_resolve_to_next_session[0] + for x in minutes_that_resolve_to_next_session)) + + self.assertEqual( + self.calendar.minute_to_session_label(minute_before_session, + direction="previous"), + previous_session_label + ) + + # make sure that exceptions are raised at the right time + with self.assertRaises(ValueError): + self.calendar.minute_to_session_label(open_minute, "asdf") + + with self.assertRaises(ValueError): + self.calendar.minute_to_session_label(minute_before_session, + direction="none") + + def test_next_prev_session(self): + session_labels = self.answers.index[1:-2] + max_idx = len(session_labels) - 1 + + # the very first session + first_session_label = self.answers.index[0] + with self.assertRaises(ValueError): + self.calendar.previous_session_label(first_session_label) + + # all the sessions in the middle + for idx, session_label in enumerate(session_labels): + if idx < max_idx: + self.assertEqual( + self.calendar.next_session_label(session_label), + session_labels[idx + 1] + ) + + if idx > 0: + self.assertEqual( + self.calendar.previous_session_label(session_label), + session_labels[idx - 1] + ) + + # the very last session + last_session_label = self.answers.index[-1] + with self.assertRaises(ValueError): + self.calendar.next_session_label(last_session_label) + + @staticmethod + def _find_full_session(calendar): + for session_label in calendar.schedule.index: + if session_label not in calendar.early_closes: + return session_label + + return None + + def test_minutes_for_period(self): + # full session + # find a session that isn't an early close. start from the first + # session, should be quick. + full_session_label = self._find_full_session(self.calendar) + if full_session_label is None: + raise ValueError("Cannot find a full session to test!") + + minutes = self.calendar.minutes_for_session(full_session_label) + _open, _close = self.calendar.open_and_close_for_session( + full_session_label + ) + + np.testing.assert_array_equal( + minutes, + pd.date_range(start=_open, end=_close, freq="min") + ) + + # early close period + early_close_session_label = self.calendar.early_closes[0] + minutes_for_early_close = \ + self.calendar.minutes_for_session(early_close_session_label) + _open, _close = self.calendar.open_and_close_for_session( + early_close_session_label + ) + + np.testing.assert_array_equal( + minutes_for_early_close, + pd.date_range(start=_open, end=_close, freq="min") + ) + + def test_sessions_in_range(self): + # pick two sessions + session_count = len(self.calendar.schedule.index) + + first_idx = session_count / 3 + second_idx = 2 * first_idx + + first_session_label = self.calendar.schedule.index[first_idx] + second_session_label = self.calendar.schedule.index[second_idx] + + answer_key = \ + self.calendar.schedule.index[first_idx:second_idx + 1] + + np.testing.assert_array_equal( + answer_key, + self.calendar.sessions_in_range(first_session_label, + second_session_label) + ) + + def _get_session_block(self): + # find and return a (full session, early close session, full session) + # block + + shortened_session = self.calendar.early_closes[0] + shortened_session_idx = \ + self.calendar.schedule.index.get_loc(shortened_session) + + session_before = self.calendar.schedule.index[ + shortened_session_idx - 1 + ] + session_after = self.calendar.schedule.index[shortened_session_idx + 1] + + return [session_before, shortened_session, session_after] + + def test_minutes_in_range(self): + sessions = self._get_session_block() + + first_open, first_close = self.calendar.open_and_close_for_session( + sessions[0] + ) + minute_before_first_open = first_open - self.one_minute + + middle_open, middle_close = \ + self.calendar.open_and_close_for_session(sessions[1]) + + last_open, last_close = self.calendar.open_and_close_for_session( + sessions[-1] + ) + minute_after_last_close = last_close + self.one_minute + + # get all the minutes between first_open and last_close + minutes1 = self.calendar.minutes_in_range( + first_open, + last_close + ) + minutes2 = self.calendar.minutes_in_range( + minute_before_first_open, + minute_after_last_close + ) + + np.testing.assert_array_equal(minutes1, minutes2) + + # manually construct the minutes + all_minutes = np.concatenate([ + pd.date_range( + start=first_open, + end=first_close, + freq="min" + ), + pd.date_range( + start=middle_open, + end=middle_close, + freq="min" + ), + pd.date_range( + start=last_open, + end=last_close, + freq="min" + ) + ]) + + np.testing.assert_array_equal(all_minutes, minutes1) + + def test_minutes_for_sessions_in_range(self): + sessions = self._get_session_block() + + minutes = self.calendar.minutes_for_sessions_in_range( + sessions[0], + sessions[-1] + ) + + # do it manually + session0_minutes = self.calendar.minutes_for_session(sessions[0]) + session1_minutes = self.calendar.minutes_for_session(sessions[1]) + session2_minutes = self.calendar.minutes_for_session(sessions[2]) + + concatenated_minutes = np.concatenate([ + session0_minutes.values, + session1_minutes.values, + session2_minutes.values + ]) + + np.testing.assert_array_equal( + concatenated_minutes, + minutes.values + ) + + def test_sessions_window(self): + sessions = self._get_session_block() + + np.testing.assert_array_equal( + self.calendar.sessions_window(sessions[0], len(sessions) - 1), + self.calendar.sessions_in_range(sessions[0], sessions[-1]) + ) + + np.testing.assert_array_equal( + self.calendar.sessions_window( + sessions[-1], + -1 * (len(sessions) - 1)), + self.calendar.sessions_in_range(sessions[0], sessions[-1]) + ) + + def test_session_distance(self): + sessions = self._get_session_block() + + self.assertEqual(2, self.calendar.session_distance(sessions[0], + sessions[-1])) + + def test_open_and_close_for_session(self): + for index, row in self.answers.iterrows(): + session_label = row.name + open_answer = row.iloc[0] + close_answer = row.iloc[1] + + found_open, found_close = \ + self.calendar.open_and_close_for_session(session_label) + + self.assertEqual(open_answer, found_open) + self.assertEqual(close_answer, found_close) + + +class NYSECalendarTestCase(ExchangeCalendarTestBase, TestCase): + + answer_key_filename = 'nyse' + calendar_class = NYSEExchangeCalendar + + def test_2012(self): + # holidays we expect: + holidays_2012 = [ + pd.Timestamp("2012-01-02", tz='UTC'), + pd.Timestamp("2012-01-16", tz='UTC'), + pd.Timestamp("2012-02-20", tz='UTC'), + pd.Timestamp("2012-04-06", tz='UTC'), + pd.Timestamp("2012-05-28", tz='UTC'), + pd.Timestamp("2012-07-04", tz='UTC'), + pd.Timestamp("2012-09-03", tz='UTC'), + pd.Timestamp("2012-11-22", tz='UTC'), + pd.Timestamp("2012-12-25", tz='UTC') + ] + + for session_label in holidays_2012: + self.assertNotIn(session_label, self.calendar.all_sessions) + + # early closes we expect: + early_closes_2012 = [ + pd.Timestamp("2012-07-03", tz='UTC'), + pd.Timestamp("2012-11-23", tz='UTC'), + pd.Timestamp("2012-12-24", tz='UTC') + ] + + for early_close_session_label in early_closes_2012: + self.assertIn(early_close_session_label, + self.calendar.early_closes) + + def test_special_holidays(self): + # 9/11 + # Sept 11, 12, 13, 14 2001 + self.assertNotIn(pd.Period("9/11/2001"), self.calendar.all_sessions) + self.assertNotIn(pd.Period("9/12/2001"), self.calendar.all_sessions) + self.assertNotIn(pd.Period("9/13/2001"), self.calendar.all_sessions) + self.assertNotIn(pd.Period("9/14/2001"), self.calendar.all_sessions) + + # Hurricane Sandy + # Oct 29, 30 2012 + self.assertNotIn(pd.Period("10/29/2012"), self.calendar.all_sessions) + self.assertNotIn(pd.Period("10/30/2012"), self.calendar.all_sessions) + + # various national days of mourning + # Gerald Ford - 1/2/2007 + self.assertNotIn(pd.Period("1/2/2007"), self.calendar.all_sessions) + + # Ronald Reagan - 6/11/2004 + self.assertNotIn(pd.Period("6/11/2004"), self.calendar.all_sessions) + + # Richard Nixon - 4/27/1994 + self.assertNotIn(pd.Period("4/27/1994"), self.calendar.all_sessions) + + def test_new_years(self): + """ + Check whether the TradingCalendar contains certain dates. + """ + # January 2012 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 6 7 + # 8 9 10 11 12 13 14 + # 15 16 17 18 19 20 21 + # 22 23 24 25 26 27 28 + # 29 30 31 + + start_session = pd.Timestamp("2012-01-02", tz='UTC') + end_session = pd.Timestamp("2013-12-31", tz='UTC') + sessions = self.calendar.sessions_in_range(start_session, end_session) + + day_after_new_years_sunday = pd.Timestamp("2012-01-02", + tz='UTC') + self.assertNotIn(day_after_new_years_sunday, sessions, + """ + If NYE falls on a weekend, {0} the Monday after is a holiday. + """.strip().format(day_after_new_years_sunday) + ) + + first_trading_day_after_new_years_sunday = pd.Timestamp("2012-01-03", + tz='UTC') + self.assertIn(first_trading_day_after_new_years_sunday, sessions, + """ + If NYE falls on a weekend, {0} the Tuesday after is the first trading day. + """.strip().format(first_trading_day_after_new_years_sunday) + ) + + # January 2013 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 + # 6 7 8 9 10 11 12 + # 13 14 15 16 17 18 19 + # 20 21 22 23 24 25 26 + # 27 28 29 30 31 + + new_years_day = pd.Timestamp("2013-01-01", tz='UTC') + self.assertNotIn(new_years_day, sessions, + """ + If NYE falls during the week, e.g. {0}, it is a holiday. + """.strip().format(new_years_day) + ) + + first_trading_day_after_new_years = pd.Timestamp("2013-01-02", + tz='UTC') + self.assertIn(first_trading_day_after_new_years, sessions, + """ + If the day after NYE falls during the week, {0} \ + is the first trading day. + """.strip().format(first_trading_day_after_new_years) + ) + + def test_thanksgiving(self): + """ + Check TradingCalendar Thanksgiving dates. + """ + # November 2005 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 + # 6 7 8 9 10 11 12 + # 13 14 15 16 17 18 19 + # 20 21 22 23 24 25 26 + # 27 28 29 30 + + start_session_label = pd.Timestamp('2005-01-01', tz='UTC') + end_session_label = pd.Timestamp('2012-12-31', tz='UTC') + sessions = self.calendar.sessions_in_range(start_session_label, + end_session_label) + + thanksgiving_with_four_weeks = pd.Timestamp("2005-11-24", tz='UTC') + + self.assertNotIn(thanksgiving_with_four_weeks, sessions, + """ + If Nov has 4 Thursdays, {0} Thanksgiving is the last Thursday. + """.strip().format(thanksgiving_with_four_weeks) + ) + + # November 2006 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 + # 5 6 7 8 9 10 11 + # 12 13 14 15 16 17 18 + # 19 20 21 22 23 24 25 + # 26 27 28 29 30 + thanksgiving_with_five_weeks = pd.Timestamp("2006-11-23", tz='UTC') + + self.assertNotIn(thanksgiving_with_five_weeks, sessions, + """ + If Nov has 5 Thursdays, {0} Thanksgiving is not the last week. + """.strip().format(thanksgiving_with_five_weeks) + ) + + first_trading_day_after_new_years_sunday = pd.Timestamp("2012-01-03", + tz='UTC') + + self.assertIn(first_trading_day_after_new_years_sunday, sessions, + """ + If NYE falls on a weekend, {0} the Tuesday after is the first trading day. + """.strip().format(first_trading_day_after_new_years_sunday) + ) + + def test_day_after_thanksgiving(self): + # November 2012 + # Su Mo Tu We Th Fr Sa + # 1 2 3 + # 4 5 6 7 8 9 10 + # 11 12 13 14 15 16 17 + # 18 19 20 21 22 23 24 + # 25 26 27 28 29 30 + fourth_friday_open = Timestamp('11/23/2012 11:00AM', tz='EST') + fourth_friday = Timestamp('11/23/2012 3:00PM', tz='EST') + self.assertTrue(self.calendar.is_open_on_minute(fourth_friday_open)) + self.assertFalse(self.calendar.is_open_on_minute(fourth_friday)) + + # November 2013 + # Su Mo Tu We Th Fr Sa + # 1 2 + # 3 4 5 6 7 8 9 + # 10 11 12 13 14 15 16 + # 17 18 19 20 21 22 23 + # 24 25 26 27 28 29 30 + fifth_friday_open = Timestamp('11/29/2013 11:00AM', tz='EST') + fifth_friday = Timestamp('11/29/2013 3:00PM', tz='EST') + self.assertTrue(self.calendar.is_open_on_minute(fifth_friday_open)) + self.assertFalse(self.calendar.is_open_on_minute(fifth_friday)) + + def test_early_close_independence_day_thursday(self): + """ + Until 2013, the market closed early the Friday after an + Independence Day on Thursday. Since then, the early close is on + Wednesday. + """ + # July 2002 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 6 + # 7 8 9 10 11 12 13 + # 14 15 16 17 18 19 20 + # 21 22 23 24 25 26 27 + # 28 29 30 31 + wednesday_before = Timestamp('7/3/2002 3:00PM', tz='EST') + friday_after_open = Timestamp('7/5/2002 11:00AM', tz='EST') + friday_after = Timestamp('7/5/2002 3:00PM', tz='EST') + self.assertTrue(self.calendar.is_open_on_minute(wednesday_before)) + self.assertTrue(self.calendar.is_open_on_minute(friday_after_open)) + self.assertFalse(self.calendar.is_open_on_minute(friday_after)) + + # July 2013 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 6 + # 7 8 9 10 11 12 13 + # 14 15 16 17 18 19 20 + # 21 22 23 24 25 26 27 + # 28 29 30 31 + wednesday_before = Timestamp('7/3/2013 3:00PM', tz='EST') + friday_after_open = Timestamp('7/5/2013 11:00AM', tz='EST') + friday_after = Timestamp('7/5/2013 3:00PM', tz='EST') + self.assertFalse(self.calendar.is_open_on_minute(wednesday_before)) + self.assertTrue(self.calendar.is_open_on_minute(friday_after_open)) + self.assertTrue(self.calendar.is_open_on_minute(friday_after)) diff --git a/tests/test_trading_schedule.py b/tests/test_trading_schedule.py deleted file mode 100644 index f897204e..00000000 --- a/tests/test_trading_schedule.py +++ /dev/null @@ -1,109 +0,0 @@ -from unittest import TestCase - -from pandas import ( - Timestamp, - date_range, - DatetimeIndex -) - -import numpy as np - -from zipline.utils.calendars import ( - get_calendar, - ExchangeTradingSchedule, - normalize_date, -) - - -class TestExchangeTradingSchedule(TestCase): - - @classmethod - def setUpClass(cls): - cls.nyse_cal = get_calendar('NYSE') - cls.nyse_exchange_schedule = ExchangeTradingSchedule(cal=cls.nyse_cal) - - def test_nyse_data_availability_time(self): - """ - Ensure that the NYSE schedule's data availability time is the market - open. - """ - # This is a time on the day after Thanksgiving when the market was open - test_dt = Timestamp('11/23/2012 11:00AM', tz='EST') - test_date = normalize_date(test_dt) - desired_data_time = Timestamp('11/23/2012 9:31AM', tz='EST') - - # Get the data availability time from the NYSE schedule - data_time = self.nyse_exchange_schedule.data_availability_time( - date=test_date - ) - - # Check the schedule answer against the hard-coded answer - self.assertEqual(data_time, desired_data_time, - "Data availability time is not the market open") - - def test_nyse_execution_time(self): - """ - Runs a series of times through both the NYSE calendar and NYSE - schedule, ensuring that the schedule and calendar agree. - """ - # Get all of the minutes in a 24-hour day - start_range = Timestamp('11/23/2012 12:00AM', tz='EST') - end_range = Timestamp('11/23/2012 11:59PM', tz='EST') - time_range = date_range(start_range, end_range, freq='Min') - - for dt in time_range: - cal_open = self.nyse_cal.is_open_on_minute(dt) - sched_exec = self.nyse_exchange_schedule.is_executing_on_minute(dt) - self.assertEqual( - cal_open, sched_exec, - "Mismatch between schedule: %s and calendar: %s at time %s" - % (cal_open, sched_exec, dt) - ) - - def test_execution_minute_window_forward(self): - dt = Timestamp("11/23/2016 15:00", tz='EST').tz_convert("UTC") - - # 61 minutes left on 11/23, closed 11/24, only 210 minutes on 11/25 - minutes = self.nyse_exchange_schedule.execution_minute_window(dt, 300) - - np.testing.assert_array_equal( - minutes[0:61], - DatetimeIndex( - start=Timestamp("2016-11-23 20:00", tz='UTC'), - end=Timestamp("2016-11-23 21:00", tz='UTC'), - freq="min" - ) - ) - - np.testing.assert_array_equal( - minutes[61:271], - DatetimeIndex( - start=Timestamp("2016-11-25 14:31", tz='UTC'), - end=Timestamp("2016-11-25 18:00", tz='UTC'), - freq="min" - ) - ) - - np.testing.assert_array_equal( - minutes[271:], - DatetimeIndex( - start=Timestamp("2016-11-28 14:31", tz='UTC'), - end=Timestamp("2016-11-28 14:59", tz='UTC'), - freq="min" - ) - ) - - def test_execution_minute_window_backward(self): - end_dt = Timestamp("2016-11-28 14:59", tz='UTC') - start_dt = Timestamp("2016-11-23 20:00", tz='UTC') - - from_end_minutes = \ - self.nyse_exchange_schedule.execution_minute_window(end_dt, -300) - - from_start_minutes = \ - self.nyse_exchange_schedule.execution_minute_window(start_dt, 300) - - np.testing.assert_array_equal( - from_end_minutes, - from_start_minutes - ) diff --git a/tests/utils/test_events.py b/tests/utils/test_events.py index f9efbcf7..62e473ac 100644 --- a/tests/utils/test_events.py +++ b/tests/utils/test_events.py @@ -1,5 +1,5 @@ # -# Copyright 2014 Quantopian, Inc. +# Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,16 +48,11 @@ from zipline.utils.events import ( Event, MAX_MONTH_RANGE, MAX_WEEK_RANGE, + TradingDayOfMonthRule, + TradingDayOfWeekRule ) -# A day known to be a half day. -HALF_DAY = datetime.datetime(year=2014, month=7, day=3) - -# A day known to be a full day. -FULL_DAY = datetime.datetime(year=2014, month=9, day=24) - - def param_range(*args): return ([n] for n in range(*args)) @@ -210,18 +205,18 @@ def minutes_for_days(ordered_days=False): # optimization in AfterOpen and BeforeClose, we rely on the fact that # the clock only ever moves forward in a simulation. For those cases, # we guarantee that the list of trading days we test is ordered. - ordered_day_list = random.sample(list(cal.all_trading_days), 500) - ordered_day_list.sort() + ordered_session_list = random.sample(list(cal.all_sessions), 500) + ordered_session_list.sort() - def day_picker(day): - return ordered_day_list[day] + def session_picker(day): + return ordered_session_list[day] else: # Other than AfterOpen and BeforeClose, we don't rely on the the nature # of the clock, so we don't care. - def day_picker(day): - return random.choice(cal.all_trading_days[:-1]) + def session_picker(day): + return random.choice(cal.all_sessions[:-1]) - return ((cal.trading_minutes_for_day(day_picker(cnt)),) + return ((cal.minutes_for_session(session_picker(cnt)),) for cnt in range(500)) @@ -250,11 +245,14 @@ class RuleTestCase(TestCase): if not self.class_: return # This is the base class testing, it is always complete. + classes_to_ignore = [TradingDayOfWeekRule, TradingDayOfMonthRule] + dem = { k for k, v in iteritems(vars(zipline.utils.events)) if isinstance(v, type) and issubclass(v, self.class_) and v is not self.class_ and + v not in classes_to_ignore and not isabstract(v) } ds = { @@ -278,18 +276,18 @@ class TestStatelessRules(RuleTestCase): cls.nyse_cal = get_calendar('NYSE') # First day of 09/2014 is closed whereas that for 10/2014 is open - cls.sept_days = cls.nyse_cal.trading_days_in_range( - pd.Timestamp('2014-09-01'), - pd.Timestamp('2014-09-30'), + cls.sept_sessions = cls.nyse_cal.sessions_in_range( + pd.Timestamp('2014-09-01', tz='UTC'), + pd.Timestamp('2014-09-30', tz='UTC'), ) - cls.oct_days = cls.nyse_cal.trading_days_in_range( - pd.Timestamp('2014-10-01'), - pd.Timestamp('2014-10-31'), + cls.oct_sessions = cls.nyse_cal.sessions_in_range( + pd.Timestamp('2014-10-01', tz='UTC'), + pd.Timestamp('2014-10-31', tz='UTC'), ) - cls.sept_week = cls.nyse_cal.trading_minutes_for_days_in_range( - datetime.date(year=2014, month=9, day=21), - datetime.date(year=2014, month=9, day=26), + cls.sept_week = cls.nyse_cal.minutes_for_sessions_in_range( + pd.Timestamp("2014-09-22", tz='UTC'), + pd.Timestamp("2014-09-26", tz='UTC') ) @subtest(minutes_for_days(), 'ms') @@ -323,14 +321,18 @@ class TestStatelessRules(RuleTestCase): else: self.assertTrue(should_trigger(m)) - @subtest(minutes_for_days(), 'ms') - def test_NotHalfDay(self, ms): - cal = get_calendar('NYSE') + def test_NotHalfDay(self): rule = NotHalfDay() - rule.cal = cal - should_trigger = rule.should_trigger - self.assertTrue(should_trigger(FULL_DAY)) - self.assertFalse(should_trigger(HALF_DAY)) + rule.cal = self.nyse_cal + + half_day_period = pd.Timestamp("2014-07-03", tz='UTC') + full_day_period = pd.Timestamp("2014-09-24", tz='UTC') + + for minute in self.nyse_cal.minutes_for_session(half_day_period): + self.assertFalse(rule.should_trigger(minute)) + + for minute in self.nyse_cal.minutes_for_session(full_day_period): + self.assertTrue(rule.should_trigger(minute)) def test_NthTradingDayOfWeek_day_zero(self): """ @@ -340,9 +342,10 @@ class TestStatelessRules(RuleTestCase): cal = get_calendar('NYSE') rule = NthTradingDayOfWeek(0) rule.cal = cal - self.assertTrue( - rule.should_trigger(self.nyse_cal.all_trading_days[0]) + first_open = self.nyse_cal.open_and_close_for_session( + self.nyse_cal.all_sessions[0] ) + self.assertTrue(first_open) @subtest(param_range(MAX_WEEK_RANGE), 'n') def test_NthTradingDayOfWeek(self, n): @@ -350,14 +353,18 @@ class TestStatelessRules(RuleTestCase): rule = NthTradingDayOfWeek(n) rule.cal = cal should_trigger = rule.should_trigger - prev_day = self.sept_week[0].date() + prev_period = self.nyse_cal.minute_to_session_label(self.sept_week[0]) n_tdays = 0 - for m in self.sept_week: - if prev_day < m.date(): - n_tdays += 1 - prev_day = m.date() + for minute in self.sept_week: + period = self.nyse_cal.minute_to_session_label( + minute, direction="none" + ) - if should_trigger(m): + if prev_period < period: + n_tdays += 1 + prev_period = period + + if should_trigger(minute): self.assertEqual(n_tdays, n) else: self.assertNotEqual(n_tdays, n) @@ -368,14 +375,17 @@ class TestStatelessRules(RuleTestCase): rule = NDaysBeforeLastTradingDayOfWeek(n) rule.cal = cal should_trigger = rule.should_trigger - for m in self.sept_week: - if should_trigger(m): + for minute in self.sept_week: + if should_trigger(minute): n_tdays = 0 - date = m.to_datetime().date() - next_date = self.nyse_cal.next_trading_day(date) - while next_date.weekday() > date.weekday(): - date = next_date - next_date = self.nyse_cal.next_trading_day(date) + session = self.nyse_cal.minute_to_session_label( + minute, + direction="none" + ) + next_session = self.nyse_cal.next_session_label(session) + while next_session.dayofweek > session.dayofweek: + session = next_session + next_session = self.nyse_cal.next_session_label(session) n_tdays += 1 self.assertEqual(n_tdays, n) @@ -397,39 +407,40 @@ class TestStatelessRules(RuleTestCase): for that week, that the trigger is recalculated for next week. """ - sim_start = pd.Timestamp('01-06-2014', tz='UTC') + \ + sim_start = pd.Timestamp('2014-01-06', tz='UTC') + \ timedelta(days=start_offset) - jan_minutes = self.nyse_cal.trading_minutes_for_days_in_range( - datetime.date(year=2014, month=1, day=6) + - timedelta(days=start_offset), - datetime.date(year=2014, month=1, day=31) + delta = timedelta(days=start_offset) + + jan_minutes = self.nyse_cal.minutes_for_sessions_in_range( + pd.Timestamp("2014-01-06", tz='UTC') + delta, + pd.Timestamp("2014-01-31", tz='UTC') ) if type == 'week_start': rule = NthTradingDayOfWeek # Expect to trigger on the first trading day of the week, plus the # offset - trigger_dates = [ + trigger_periods = [ pd.Timestamp('2014-01-06', tz='UTC'), pd.Timestamp('2014-01-13', tz='UTC'), pd.Timestamp('2014-01-21', tz='UTC'), pd.Timestamp('2014-01-27', tz='UTC'), ] - trigger_dates = \ - [x + timedelta(days=rule_offset) for x in trigger_dates] + trigger_periods = \ + [x + timedelta(days=rule_offset) for x in trigger_periods] else: rule = NDaysBeforeLastTradingDayOfWeek # Expect to trigger on the last trading day of the week, minus the # offset - trigger_dates = [ + trigger_periods = [ pd.Timestamp('2014-01-10', tz='UTC'), pd.Timestamp('2014-01-17', tz='UTC'), pd.Timestamp('2014-01-24', tz='UTC'), pd.Timestamp('2014-01-31', tz='UTC'), ] - trigger_dates = \ - [x - timedelta(days=rule_offset) for x in trigger_dates] + trigger_periods = \ + [x - timedelta(days=rule_offset) for x in trigger_periods] rule.cal = self.nyse_cal should_trigger = rule(rule_offset).should_trigger @@ -437,23 +448,23 @@ class TestStatelessRules(RuleTestCase): # If offset is 4, there is not enough trading days in the short week, # and so it should not trigger if rule_offset == 4: - del trigger_dates[2] + del trigger_periods[2] # Filter out trigger dates that happen before the simulation starts - trigger_dates = [x for x in trigger_dates if x >= sim_start] + trigger_periods = [x for x in trigger_periods if x >= sim_start] # Get all the minutes on the trigger dates - trigger_dts = self.nyse_cal.trading_minutes_for_day(trigger_dates[0]) - for dt in trigger_dates[1:]: - trigger_dts += self.nyse_cal.trading_minutes_for_day(dt) + trigger_minutes = self.nyse_cal.minutes_for_session(trigger_periods[0]) + for period in trigger_periods[1:]: + trigger_minutes += self.nyse_cal.minutes_for_session(period) - expected_n_triggered = len(trigger_dts) - trigger_dts = iter(trigger_dts) + expected_n_triggered = len(trigger_minutes) + trigger_minutes_iter = iter(trigger_minutes) n_triggered = 0 for m in jan_minutes: if should_trigger(m): - self.assertEqual(m, next(trigger_dts)) + self.assertEqual(m, next(trigger_minutes_iter)) n_triggered += 1 self.assertEqual(n_triggered, expected_n_triggered) @@ -471,9 +482,9 @@ class TestStatelessRules(RuleTestCase): should_trigger = composed_rule.should_trigger - week_minutes = self.nyse_cal.trading_minutes_for_days_in_range( - datetime.date(year=2014, month=1, day=6), - datetime.date(year=2014, month=1, day=10) + week_minutes = self.nyse_cal.minutes_for_sessions_in_range( + pd.Timestamp("2014-01-06", tz='UTC'), + pd.Timestamp("2014-01-10", tz='UTC') ) dt = pd.Timestamp('2014-01-06 14:30:00', tz='UTC') @@ -495,9 +506,9 @@ class TestStatelessRules(RuleTestCase): rule = NthTradingDayOfMonth(n) rule.cal = cal should_trigger = rule.should_trigger - for days_list in (self.sept_days, self.oct_days): - for n_tdays, d in enumerate(days_list): - for m in self.nyse_cal.trading_minutes_for_day(d): + for sessions_list in (self.sept_sessions, self.oct_sessions): + for n_tdays, session in enumerate(sessions_list): + for m in self.nyse_cal.minutes_for_session(session): if should_trigger(m): self.assertEqual(n_tdays, n) else: @@ -509,8 +520,8 @@ class TestStatelessRules(RuleTestCase): rule = NDaysBeforeLastTradingDayOfMonth(n) rule.cal = cal should_trigger = rule.should_trigger - for n_days_before, d in enumerate(reversed(self.sept_days)): - for m in self.nyse_cal.trading_minutes_for_day(d): + for n_days_before, session in enumerate(reversed(self.oct_sessions)): + for m in self.nyse_cal.minutes_for_session(session): if should_trigger(m): self.assertEqual(n_days_before, n) else: diff --git a/zipline/_protocol.pyx b/zipline/_protocol.pyx index 02d3074c..122f5f82 100644 --- a/zipline/_protocol.pyx +++ b/zipline/_protocol.pyx @@ -224,7 +224,7 @@ cdef class BarData: if self._adjust_minutes: dt = \ - self.data_portal.trading_schedule.previous_execution_minute(dt) + self.data_portal.trading_calendar.previous_minute(dt) return dt diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 9c135bc1..36b0db0e 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -56,8 +56,7 @@ from zipline.errors import ( CannotOrderDelistedAsset, UnsupportedCancelPolicy, SetCancelPolicyPostInit, - OrderInBeforeTradingStart, - ScheduleFunctionWithoutCalendar, + OrderInBeforeTradingStart ) from zipline.finance.trading import TradingEnvironment from zipline.finance.blotter import Blotter @@ -98,10 +97,8 @@ from zipline.utils.api_support import ( from zipline.utils.input_validation import ensure_upper_case, error_keywords from zipline.utils.cache import CachedObject, Expired -from zipline.utils.calendars import ( - default_nyse_schedule, - ExchangeTradingSchedule, -) +from zipline.utils.calendars import get_calendar + import zipline.utils.events from zipline.utils.events import ( EventManager, @@ -282,9 +279,9 @@ class TradingAlgorithm(object): ) # If a schedule has been provided, pop it. Otherwise, use NYSE. - self.trading_schedule = kwargs.pop( - 'trading_schedule', - default_nyse_schedule, + self.trading_calendar = kwargs.pop( + 'trading_calendar', + get_calendar("NYSE") ) # set the capital base @@ -295,11 +292,7 @@ class TradingAlgorithm(object): capital_base=self.capital_base, start=kwargs.pop('start', None), end=kwargs.pop('end', None), - trading_schedule=self.trading_schedule, - ) - else: - self.sim_params.update_internal_from_trading_schedule( - self.trading_schedule + trading_calendar=self.trading_calendar, ) self.perf_tracker = None @@ -427,7 +420,7 @@ class TradingAlgorithm(object): if get_loader is not None: self.engine = SimplePipelineEngine( get_loader, - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, self.asset_finder, ) else: @@ -500,8 +493,8 @@ class TradingAlgorithm(object): If the clock property is not set, then create one based on frequency. """ if self.sim_params.data_frequency == 'minute': - trading_o_and_c = self.trading_schedule.schedule.ix[ - self.sim_params.trading_days] + trading_o_and_c = self.trading_calendar.schedule.ix[ + self.sim_params.sessions] market_opens = trading_o_and_c['market_open'].values.astype( 'datetime64[ns]').astype(np.int64) market_closes = trading_o_and_c['market_close'].values.astype( @@ -510,21 +503,21 @@ class TradingAlgorithm(object): minutely_emission = self.sim_params.emission_rate == "minute" clock = MinuteSimulationClock( - self.sim_params.trading_days, + self.sim_params.sessions, market_opens, market_closes, minutely_emission ) return clock else: - return DailySimulationClock(self.sim_params.trading_days) + return DailySimulationClock(self.sim_params.sessions) def _create_benchmark_source(self): return BenchmarkSource( benchmark_sid=self.benchmark_sid, env=self.trading_environment, - trading_schedule=self.trading_schedule, - trading_days=self.sim_params.trading_days, + trading_calendar=self.trading_calendar, + sessions=self.sim_params.sessions, data_portal=self.data_portal, emission_rate=self.sim_params.emission_rate, ) @@ -538,12 +531,12 @@ class TradingAlgorithm(object): # None so that it will be overwritten here. self.perf_tracker = PerformanceTracker( sim_params=self.sim_params, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, env=self.trading_environment, ) # Set the dt initially to the period start by forcing it to change. - self.on_dt_changed(self.sim_params.period_start) + self.on_dt_changed(self.sim_params.start_session) if not self.initialized: self.initialize(*self.initialize_args, **self.initialize_kwargs) @@ -613,12 +606,9 @@ class TradingAlgorithm(object): # For compatibility with existing examples allow start/end # to be inferred. if overwrite_sim_params: - self.sim_params.period_start = data.major_axis[0] - self.sim_params.period_end = data.major_axis[-1] - # Changing period_start and period_close might require - # updating of first_open and last_close. - self.sim_params.update_internal_from_trading_schedule( - trading_schedule=self.trading_schedule + self.sim_params = self.sim_params.create_new( + data.major_axis[0], + data.major_axis[1] ) copy_panel = data.rename( @@ -637,12 +627,12 @@ class TradingAlgorithm(object): ) ) equity_daily_reader = PanelDailyBarReader( - self.trading_schedule.all_execution_days, + self.trading_calendar.all_sessions, copy_panel, ) self.data_portal = DataPortal( self.asset_finder, - self.trading_schedule, + self.trading_calendar, first_trading_day=equity_daily_reader.first_trading_day, equity_daily_reader=equity_daily_reader, ) @@ -743,8 +733,8 @@ class TradingAlgorithm(object): elif new_sids: frame_to_write = make_simple_equity_info( new_sids, - start_date=self.sim_params.period_start, - end_date=self.sim_params.period_end, + start_date=self.sim_params.start_session, + end_date=self.sim_params.end_session, symbols=map(str, new_sids), ) elif new_symbols: @@ -754,7 +744,7 @@ class TradingAlgorithm(object): frame_to_write = make_simple_equity_info( sids=fake_sids, start_date=as_of_date, - end_date=self.sim_params.period_end, + end_date=self.sim_params.end_session, symbols=new_symbols, ) else: @@ -914,9 +904,9 @@ class TradingAlgorithm(object): pre_func, post_func, self.asset_finder, - self.trading_schedule.day, - self.sim_params.period_start, - self.sim_params.period_end, + self.trading_calendar.day, + self.sim_params.start_session, + self.sim_params.end_session, date_column, date_format, timezone, @@ -992,11 +982,7 @@ class TradingAlgorithm(object): # Note that the ExchangeTradingSchedule is currently the only # TradingSchedule class, so this is unlikely to be hit # TODO The calendar should be a required arg for schedule_function - if not isinstance(self.trading_schedule, ExchangeTradingSchedule): - raise ScheduleFunctionWithoutCalendar( - schedule=self.trading_schedule - ) - cal = self.trading_schedule._exchange_calendar + cal = self.trading_calendar self.add_event( make_eventrule(date_rule, time_rule, cal, half_days), @@ -1074,9 +1060,9 @@ class TradingAlgorithm(object): :func:`zipline.api.set_symbol_lookup_date` """ # If the user has not set the symbol lookup date, - # use the period_end as the date for sybmol->sid resolution. + # use the end_session as the date for sybmol->sid resolution. _lookup_date = self._symbol_lookup_date if self._symbol_lookup_date is not None \ - else self.sim_params.period_end + else self.sim_params.end_session return self.asset_finder.lookup_symbol( symbol_str, @@ -1963,7 +1949,7 @@ class TradingAlgorithm(object): # If we are in before_trading_start, we need to get the window # as of the previous market minute adjusted_dt = \ - self.data_portal.trading_schedule.previous_execution_minute( + self.trading_calendar.previous_minute( self.datetime ) @@ -2223,7 +2209,7 @@ class TradingAlgorithm(object): # day. return pd.DataFrame(index=[], columns=data.columns) - def _run_pipeline(self, pipeline, start_date, chunksize): + def _run_pipeline(self, pipeline, start_session, chunksize): """ Compute `pipeline`, providing values for at least `start_date`. @@ -2241,19 +2227,25 @@ class TradingAlgorithm(object): -------- PipelineEngine.run_pipeline """ - days = self.trading_schedule.all_execution_days + sessions = self.trading_calendar.all_sessions # Load data starting from the previous trading day... - start_date_loc = days.get_loc(start_date) + start_date_loc = sessions.get_loc(start_session) # ...continuing until either the day before the simulation end, or # until chunksize days of data have been loaded. - sim_end = self.sim_params.last_close.normalize() - end_loc = min(start_date_loc + chunksize, days.get_loc(sim_end)) - end_date = days[end_loc] + sim_end_session = self.sim_params.end_session + + end_loc = min( + start_date_loc + chunksize, + sessions.get_loc(sim_end_session) + ) + + end_session = sessions[end_loc] return \ - self.engine.run_pipeline(pipeline, start_date, end_date), end_date + self.engine.run_pipeline(pipeline, start_session, end_session), \ + end_session ################## # End Pipeline API diff --git a/zipline/data/bundles/core.py b/zipline/data/bundles/core.py index 56167090..9d1bf611 100644 --- a/zipline/data/bundles/core.py +++ b/zipline/data/bundles/core.py @@ -32,7 +32,7 @@ from zipline.utils.preprocess import preprocess from zipline.utils.calendars import get_calendar nyse_cal = get_calendar('NYSE') -trading_days = nyse_cal.all_trading_days +trading_days = nyse_cal.all_sessions open_and_closes = nyse_cal.schedule diff --git a/zipline/data/data_portal.py b/zipline/data/data_portal.py index 08e04ee2..50850015 100644 --- a/zipline/data/data_portal.py +++ b/zipline/data/data_portal.py @@ -467,9 +467,10 @@ class DataPortal(object): Parameters ---------- - env : TradingEnvironment - The trading environment for the simulation. This includes the trading - calendar and benchmark data. + asset_finder : zipline.assets.assets.AssetFinder + The AssetFinder instance used to resolve assets. + trading_calendar: zipline.utils.calendar.exchange_calendar.TradingCalendar + The calendar instance used to provide minute->session information. first_trading_day : pd.Timestamp The first trading day for the simulation. equity_daily_reader : BcolzDailyBarReader, optional @@ -496,7 +497,7 @@ class DataPortal(object): """ def __init__(self, asset_finder, - trading_schedule, + trading_calendar, first_trading_day, equity_daily_reader=None, equity_minute_reader=None, @@ -504,7 +505,7 @@ class DataPortal(object): future_minute_reader=None, adjustment_reader=None): - self.trading_schedule = trading_schedule + self.trading_calendar = trading_calendar self.asset_finder = asset_finder self.views = {} @@ -536,7 +537,7 @@ class DataPortal(object): self._equity_daily_reader = equity_daily_reader if self._equity_daily_reader is not None: self._equity_history_loader = USEquityDailyHistoryLoader( - self.trading_schedule, + self.trading_calendar, self._equity_daily_reader, self._adjustment_reader ) @@ -546,10 +547,10 @@ class DataPortal(object): if self._equity_minute_reader is not None: self._equity_daily_aggregator = DailyHistoryAggregator( - self.trading_schedule.schedule.market_open, + self.trading_calendar.schedule.market_open, self._equity_minute_reader) self._equity_minute_history_loader = USEquityMinuteHistoryLoader( - self.trading_schedule, + self.trading_calendar, self._equity_minute_reader, self._adjustment_reader ) @@ -560,19 +561,19 @@ class DataPortal(object): # Get the first trading minute self._first_trading_minute, _ = ( - self.trading_schedule.start_and_end(self._first_trading_day) + self.trading_calendar.open_and_close_for_session( + self._first_trading_day + ) if self._first_trading_day is not None else (None, None) ) # Store the locs of the first day and first minute self._first_trading_day_loc = ( - self.trading_schedule.all_execution_days.get_loc( - self.trading_schedule.session_date(self._first_trading_day) - ) + self.trading_calendar.all_sessions.get_loc(self._first_trading_day) if self._first_trading_day is not None else None ) self._first_trading_minute_loc = ( - self.trading_schedule.all_execution_minutes.get_loc( + self.trading_calendar.all_minutes.get_loc( self._first_trading_minute ) if self._first_trading_minute is not None else None @@ -612,9 +613,9 @@ class DataPortal(object): # asset -> df. In other words, # self.augmented_sources_map['days_to_cover']['AAPL'] gives us the df # holding that data. - source_date_index = self.trading_schedule.execution_days_in_range( - start=sim_params.period_start, - end=sim_params.period_end + source_date_index = self.trading_calendar.sessions_in_range( + sim_params.start_session, + sim_params.end_session ) # Break the source_df up into one dataframe per sid. This lets @@ -1031,13 +1032,13 @@ class DataPortal(object): spot_value=value ) else: - found_dt -= self.trading_schedule.day + found_dt -= self.trading_calendar.day except NoDataOnDate: return np.nan @remember_last def _get_days_for_window(self, end_date, bar_count): - tds = self.trading_schedule.all_execution_days + tds = self.trading_calendar.all_sessions end_loc = tds.get_loc(end_date) start_loc = end_loc - bar_count + 1 if start_loc < self._first_trading_day_loc: @@ -1096,7 +1097,7 @@ class DataPortal(object): # get all the minutes for the days NOT including today for day in days_for_window[:-1]: - minutes = self.trading_schedule.execution_minutes_for_day(day) + minutes = self.sessions_in_range.minutes_for_session(day) values_for_day = np.zeros(len(minutes), dtype=np.float64) @@ -1111,7 +1112,7 @@ class DataPortal(object): # get the minutes for today last_day_minutes = pd.date_range( - start=self.trading_schedule.start_and_end(end_dt)[0], + start=self.trading_calendar.open_and_close_for_session(end_dt)[0], end=end_dt, freq="T" ) @@ -1190,9 +1191,9 @@ class DataPortal(object): def _handle_history_out_of_bounds(self, bar_count): suggested_start_day = ( - self.trading_schedule.all_execution_minutes[ + self.trading_calendar.all_minutes[ self._first_trading_minute_loc + bar_count - ] + self.trading_schedule.day + ] + self.trading_calendar.day ).date() raise HistoryWindowStartsBeforeData( @@ -1209,7 +1210,7 @@ class DataPortal(object): """ # get all the minutes for this window try: - minutes_for_window = self.trading_schedule.execution_minute_window( + minutes_for_window = self.trading_calendar.minutes_window( end_dt, -bar_count ) except KeyError: @@ -1728,21 +1729,29 @@ class DataPortal(object): # we get all the minutes for the last (bars - 1) days, then add # all the minutes so far today. the +2 is to account for ignoring # today, and the previous day, in doing the math. - previous_day = \ - self.trading_schedule.previous_execution_day(ending_minute) - days = self.trading_schedule.execution_days_in_range( - self.trading_schedule.add_execution_days(-days_count + 2, - previous_day), - previous_day, + session_for_minute = self.trading_calendar.minute_to_session_label( + ending_minute + ) + previous_session = self.trading_calendar.previous_session_label( + session_for_minute + ) + + sessions = self.trading_calendar.sessions_in_range( + self.trading_calendar.sessions_window(previous_session, + -days_count + 2)[0], + previous_session, ) minutes_count = sum( - 210 if day in self.trading_schedule.early_ends - else 390 for day in days + len(self.trading_calendar.minutes_for_session(session)) + for session in sessions ) # add the minutes for today - today_open = self.trading_schedule.start_and_end(ending_minute)[0] + today_open = self.trading_calendar.open_and_close_for_session( + session_for_minute + )[0] + minutes_count += \ ((ending_minute - today_open).total_seconds() // 60) + 1 diff --git a/zipline/data/loader.py b/zipline/data/loader.py index 62170a38..3bb92e24 100644 --- a/zipline/data/loader.py +++ b/zipline/data/loader.py @@ -47,7 +47,7 @@ ONE_HOUR = pd.Timedelta(hours=1) nyse_cal = get_calendar('NYSE') trading_day_nyse = nyse_cal.day -trading_days_nyse = nyse_cal.all_trading_days +trading_days_nyse = nyse_cal.all_sessions def last_modified_time(path): diff --git a/zipline/data/minute_bars.py b/zipline/data/minute_bars.py index 2cedd1b2..e35a73c9 100644 --- a/zipline/data/minute_bars.py +++ b/zipline/data/minute_bars.py @@ -280,13 +280,14 @@ class BcolzMinuteBarWriter(object): minutes_per_day, ohlc_ratio=OHLC_RATIO, expectedlen=DEFAULT_EXPECTEDLEN): + self._rootdir = rootdir self._first_trading_day = first_trading_day self._market_opens = market_opens[ market_opens.index.slice_indexer(start=self._first_trading_day)] self._market_closes = market_closes[ market_closes.index.slice_indexer(start=self._first_trading_day)] - self._trading_days = market_opens.index + self._trading_days = self._market_opens.index self._minutes_per_day = minutes_per_day self._expectedlen = expectedlen self._ohlc_ratio = ohlc_ratio diff --git a/zipline/data/us_equity_loader.py b/zipline/data/us_equity_loader.py index c522b6ab..355f5954 100644 --- a/zipline/data/us_equity_loader.py +++ b/zipline/data/us_equity_loader.py @@ -75,6 +75,8 @@ class USEquityHistoryLoader(with_metaclass(ABCMeta)): Parameters ---------- + trading_calendar: TradingCalendar + Contains the grouping logic needed to assign minutes to periods. reader : DailyBarReader, MinuteBarReader Reader for pricing bars. adjustment_reader : SQLiteAdjustmentReader @@ -82,9 +84,9 @@ class USEquityHistoryLoader(with_metaclass(ABCMeta)): """ FIELDS = ('open', 'high', 'low', 'close', 'volume') - def __init__(self, trading_schedule, reader, adjustment_reader, + def __init__(self, trading_calendar, reader, adjustment_reader, sid_cache_size=1000): - self.trading_schedule = trading_schedule + self.trading_calendar = trading_calendar self._reader = reader self._adjustments_reader = adjustment_reader self._window_blocks = { @@ -404,7 +406,7 @@ class USEquityMinuteHistoryLoader(USEquityHistoryLoader): @lazyval def _calendar(self): - mm = self.trading_schedule.all_execution_minutes + mm = self.trading_calendar.all_minutes return mm[mm.slice_indexer(start=self._reader.first_trading_day, end=self._reader.last_available_dt)] diff --git a/zipline/data/us_equity_pricing.py b/zipline/data/us_equity_pricing.py index e388f2fd..541c31f5 100644 --- a/zipline/data/us_equity_pricing.py +++ b/zipline/data/us_equity_pricing.py @@ -189,7 +189,7 @@ class BcolzDailyBarWriter(object): ---------- filename : str The location at which we should write our output. - calendar : pandas.DatetimeIndex + sessions : pandas.DatetimeIndex Calendar to use to compute asset calendar offsets. See Also @@ -204,8 +204,9 @@ class BcolzDailyBarWriter(object): 'volume': float64, } - def __init__(self, filename, calendar): + def __init__(self, filename, sessions, calendar): self._filename = filename + self._sessions = sessions self._calendar = calendar @property @@ -299,7 +300,7 @@ class BcolzDailyBarWriter(object): } earliest_date = None - calendar = self._calendar + sessions = self._sessions if assets is not None: @apply @@ -342,8 +343,10 @@ class BcolzDailyBarWriter(object): # in the stored data and the first date of **this** asset. This # offset used for output alignment by the reader. asset_first_day = table['day'][0] - calendar_offset[asset_key] = calendar.get_loc( - Timestamp(asset_first_day, unit='s', tz='UTC'), + calendar_offset[asset_key] = sessions.get_loc( + self._calendar.minute_to_session_label( + Timestamp(asset_first_day, unit='s', tz='UTC') + ) ) # This writes the table to disk. @@ -363,7 +366,7 @@ class BcolzDailyBarWriter(object): full_table.attrs['first_row'] = first_row full_table.attrs['last_row'] = last_row full_table.attrs['calendar_offset'] = calendar_offset - full_table.attrs['calendar'] = calendar.asi8.tolist() + full_table.attrs['calendar'] = sessions.asi8.tolist() full_table.flush() return full_table diff --git a/zipline/errors.py b/zipline/errors.py index 06d4a694..2fadc0c2 100644 --- a/zipline/errors.py +++ b/zipline/errors.py @@ -642,7 +642,7 @@ class InvalidCalendarName(ZiplineError): Raised when a calendar with an invalid name is requested. """ msg = ( - "The requested ExchangeCalendar, {calendar_name}, does not exist." + "The requested TradingCalendar, {calendar_name}, does not exist." ) diff --git a/zipline/finance/performance/position_tracker.py b/zipline/finance/performance/position_tracker.py index 7a1a2ffa..7983e8f0 100644 --- a/zipline/finance/performance/position_tracker.py +++ b/zipline/finance/performance/position_tracker.py @@ -384,7 +384,7 @@ class PositionTracker(object): last_sale_price = data_portal.get_adjusted_value( asset, 'price', - data_portal.trading_schedule.previous_execution_minute(dt), + data_portal.trading_calendar.previous_minute(dt), dt, self.data_frequency ) diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index 1d4c3630..f4a3cb82 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -60,7 +60,6 @@ Performance Tracking from __future__ import division import logbook -from datetime import datetime import pandas as pd from pandas.tseries.tools import normalize_date @@ -78,52 +77,52 @@ class PerformanceTracker(object): """ Tracks the performance of the algorithm. """ - def __init__(self, sim_params, trading_schedule, env): + def __init__(self, sim_params, trading_calendar, env): self.sim_params = sim_params - self.trading_schedule = trading_schedule + self.trading_calendar = trading_calendar self.asset_finder = env.asset_finder self.treasury_curves = env.treasury_curves - self.period_start = self.sim_params.period_start - self.period_end = self.sim_params.period_end + self.period_start = self.sim_params.start_session + self.period_end = self.sim_params.end_session self.last_close = self.sim_params.last_close - first_open = self.sim_params.first_open.tz_convert(trading_schedule.tz) - self.day = pd.Timestamp(datetime(first_open.year, first_open.month, - first_open.day), tz='UTC') - self.market_open, self.market_close = trading_schedule.start_and_end( - self.day - ) - self.total_days = self.sim_params.days_in_period + self._current_session = self.sim_params.start_session + + self.market_open, self.market_close = \ + self.trading_calendar.open_and_close_for_session( + self._current_session + ) + + self.total_session_count = len(self.sim_params.sessions) self.capital_base = self.sim_params.capital_base self.emission_rate = sim_params.emission_rate - self.trading_days = trading_schedule.trading_dates( - self.period_start, self.period_end - ) - self.position_tracker = PositionTracker( asset_finder=env.asset_finder, - data_frequency=self.sim_params.data_frequency) + data_frequency=self.sim_params.data_frequency + ) if self.emission_rate == 'daily': self.all_benchmark_returns = pd.Series( - index=self.trading_days) + index=self.sim_params.sessions + ) self.cumulative_risk_metrics = \ risk.RiskMetricsCumulative( self.sim_params, self.treasury_curves, - self.trading_schedule + self.trading_calendar ) elif self.emission_rate == 'minute': self.all_benchmark_returns = pd.Series(index=pd.date_range( self.sim_params.first_open, self.sim_params.last_close, - freq='Min')) + freq='Min') + ) self.cumulative_risk_metrics = \ risk.RiskMetricsCumulative( self.sim_params, self.treasury_curves, - self.trading_schedule, + self.trading_calendar, create_first_day_stats=True ) @@ -165,7 +164,7 @@ class PerformanceTracker(object): self.saved_dt = self.period_start # one indexed so that we reach 100% - self.day_count = 0.0 + self.session_count = 0.0 self.txn_count = 0 self.account_needs_update = True @@ -182,7 +181,7 @@ class PerformanceTracker(object): # Fake a value return 1.0 elif self.emission_rate == 'daily': - return self.day_count / self.total_days + return self.session_count / self.total_session_count def set_date(self, date): if self.emission_rate == 'minute': @@ -280,7 +279,7 @@ class PerformanceTracker(object): if txn: self.process_transaction(txn) - def check_upcoming_dividends(self, next_trading_day, adjustment_reader): + def check_upcoming_dividends(self, next_session, adjustment_reader): """ Check if we currently own any stocks with dividends whose ex_date is the next trading day. Track how much we should be payed on those @@ -301,13 +300,13 @@ class PerformanceTracker(object): if held_sids: cash_dividends = adjustment_reader.get_dividends_with_ex_date( held_sids, - next_trading_day, + next_session, self.asset_finder ) stock_dividends = adjustment_reader.\ get_stock_dividends_with_ex_date( held_sids, - next_trading_day, + next_session, self.asset_finder ) @@ -316,7 +315,7 @@ class PerformanceTracker(object): stock_dividends ) - net_cash_payment = position_tracker.pay_dividends(next_trading_day) + net_cash_payment = position_tracker.pay_dividends(next_session) if not net_cash_payment: return @@ -368,7 +367,7 @@ class PerformanceTracker(object): _______ A daily perf packet. """ - completed_date = self.day + completed_session = self._current_session if self.emission_rate == 'daily': # this method is called for both minutely and daily emissions, but @@ -378,25 +377,25 @@ class PerformanceTracker(object): self.update_performance() account = self.get_account(False) - benchmark_value = self.all_benchmark_returns[completed_date] + benchmark_value = self.all_benchmark_returns[completed_session] self.cumulative_risk_metrics.update( - completed_date, + completed_session, self.todays_performance.returns, benchmark_value, account.leverage) # increment the day counter before we move markers forward. - self.day_count += 1.0 + self.session_count += 1.0 # Get the next trading day and, if it is past the bounds of this # simulation, return the daily perf packet try: - next_trading_day = self.trading_schedule.next_execution_day( - completed_date + next_session = self.trading_calendar.next_session_label( + completed_session ) except NoFurtherDataError: - next_trading_day = None + next_session = None # Take a snapshot of our current performance to return to the # browser. @@ -408,24 +407,26 @@ class PerformanceTracker(object): if self.market_close >= self.last_close: return daily_update + # If the next trading day is irrelevant, then return the daily packet + if (next_session is None) or (next_session >= self.last_close): + return daily_update + # move the market day markers forward # TODO Is this redundant with next_trading_day above? - self.day = self.trading_schedule.next_execution_day(self.day) + self._current_session = next_session self.market_open, self.market_close = \ - self.trading_schedule.start_and_end(self.day) + self.trading_calendar.open_and_close_for_session( + self._current_session + ) # Roll over positions to current day. self.todays_performance.rollover() self.todays_performance.period_open = self.market_open self.todays_performance.period_close = self.market_close - # If the next trading day is irrelevant, then return the daily packet - if (next_trading_day is None) or (next_trading_day >= self.last_close): - return daily_update - # Check for any dividends, then return the daily perf packet self.check_upcoming_dividends( - next_trading_day=next_trading_day, + next_session=next_session, adjustment_reader=data_portal._adjustment_reader ) @@ -438,7 +439,8 @@ class PerformanceTracker(object): """ log_msg = "Simulated {n} trading days out of {m}." - log.info(log_msg.format(n=int(self.day_count), m=self.total_days)) + log.info(log_msg.format(n=int(self.session_count), + m=self.total_session_count)) log.info("first open: {d}".format( d=self.sim_params.first_open)) log.info("last close: {d}".format( @@ -451,12 +453,13 @@ class PerformanceTracker(object): index=self.cumulative_risk_metrics.cont_index, data=self.cumulative_risk_metrics.algorithm_returns_cont) acl = self.cumulative_risk_metrics.algorithm_cumulative_leverages + self.risk_report = risk.RiskReport( ars, self.sim_params, benchmark_returns=bms, algorithm_leverages=acl, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.treasury_curves, ) diff --git a/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index 3a0baf7c..11a7435f 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -86,38 +86,34 @@ class RiskMetricsCumulative(object): 'information', ) - def __init__(self, sim_params, treasury_curves, trading_schedule, + def __init__(self, sim_params, treasury_curves, trading_calendar, create_first_day_stats=False): self.treasury_curves = treasury_curves - self.trading_schedule = trading_schedule - self.start_date = sim_params.period_start.replace( - hour=0, minute=0, second=0, microsecond=0 - ) - self.end_date = sim_params.period_end.replace( - hour=0, minute=0, second=0, microsecond=0 - ) + self.trading_calendar = trading_calendar + self.start_session = sim_params.start_session + self.end_session = sim_params.end_session - self.trading_days = trading_schedule.trading_dates( - self.start_date, self.end_date + self.sessions = trading_calendar.sessions_in_range( + self.start_session, self.end_session ) # Hold on to the trading day before the start, # used for index of the zero return value when forcing returns # on the first day. - self.day_before_start = self.start_date - self.trading_days.freq + self.day_before_start = self.start_session - self.sessions.freq - last_day = normalize_date(sim_params.period_end) - if last_day not in self.trading_days: + last_day = normalize_date(sim_params.end_session) + if last_day not in self.sessions: last_day = pd.tseries.index.DatetimeIndex( [last_day] ) - self.trading_days = self.trading_days.append(last_day) + self.sessions = self.sessions.append(last_day) self.sim_params = sim_params self.create_first_day_stats = create_first_day_stats - cont_index = self.trading_days + cont_index = self.sessions self.cont_index = cont_index self.cont_len = len(self.cont_index) @@ -164,7 +160,7 @@ class RiskMetricsCumulative(object): self.max_leverages = empty_cont.copy() self.max_leverage = 0 self.current_max = -np.inf - self.daily_treasury = pd.Series(index=self.trading_days) + self.daily_treasury = pd.Series(index=self.sessions) self.treasury_period_return = np.nan self.num_trading_days = 0 @@ -249,8 +245,8 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" message = message.format( bm_count=len(self.benchmark_returns), algo_count=len(self.algorithm_returns), - start=self.start_date, - end=self.end_date, + start=self.start_session, + end=self.end_session, dt=dt ) raise Exception(message) @@ -269,9 +265,9 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" if np.isnan(self.daily_treasury[treasury_end]): treasury_period_return = choose_treasury( self.treasury_curves, - self.start_date, + self.start_session, treasury_end, - self.trading_schedule, + self.trading_calendar, ) self.daily_treasury[treasury_end] = treasury_period_return self.treasury_period_return = self.daily_treasury[treasury_end] diff --git a/zipline/finance/risk/period.py b/zipline/finance/risk/period.py index ec3c1428..be597b8e 100644 --- a/zipline/finance/risk/period.py +++ b/zipline/finance/risk/period.py @@ -41,12 +41,12 @@ choose_treasury = functools.partial(risk.choose_treasury, class RiskMetricsPeriod(object): - def __init__(self, start_date, end_date, returns, trading_schedule, + def __init__(self, start_session, end_session, returns, trading_calendar, treasury_curves, benchmark_returns, algorithm_leverages=None): - if treasury_curves.index[-1] >= start_date: - mask = ((treasury_curves.index >= start_date) & - (treasury_curves.index <= end_date)) + if treasury_curves.index[-1] >= start_session: + mask = ((treasury_curves.index >= start_session) & + (treasury_curves.index <= end_session)) self.treasury_curves = treasury_curves[mask] else: @@ -54,16 +54,16 @@ class RiskMetricsPeriod(object): # so we'll use the last available treasury curve self.treasury_curves = treasury_curves[-1:] - self.start_date = start_date - self.end_date = end_date - self.trading_schedule = trading_schedule + self._start_session = start_session + self._end_session = end_session + self.trading_calendar = trading_calendar - trading_dates = trading_schedule.trading_dates( - start=self.start_date, - end=self.end_date, + trading_sessions = trading_calendar.sessions_in_range( + self._start_session, + self._end_session, ) self.algorithm_returns = self.mask_returns_to_period(returns, - trading_dates) + trading_sessions) # Benchmark needs to be masked to the same dates as the algo returns self.benchmark_returns = self.mask_returns_to_period( @@ -75,7 +75,6 @@ class RiskMetricsPeriod(object): self.calculate_metrics() def calculate_metrics(self): - self.benchmark_period_returns = \ self.calculate_period_returns(self.benchmark_returns) @@ -90,8 +89,8 @@ class RiskMetricsPeriod(object): message = message.format( bm_count=len(self.benchmark_returns), algo_count=len(self.algorithm_returns), - start=self.start_date, - end=self.end_date + start=self._start_session, + end=self._end_session ) raise Exception(message) @@ -108,9 +107,9 @@ class RiskMetricsPeriod(object): self.algorithm_returns) self.treasury_period_return = choose_treasury( self.treasury_curves, - self.start_date, - self.end_date, - self.trading_schedule, + self._start_session, + self._end_session, + self.trading_calendar, ) self.sharpe = self.calculate_sharpe() # The consumer currently expects a 0.0 value for sharpe in period, @@ -137,7 +136,7 @@ class RiskMetricsPeriod(object): Creates a dictionary representing the state of the risk report. Returns a dict object of the form: """ - period_label = self.end_date.strftime("%Y-%m") + period_label = self._end_session.strftime("%Y-%m") rval = { 'trading_days': self.num_trading_days, 'benchmark_volatility': self.benchmark_volatility, @@ -198,8 +197,8 @@ class RiskMetricsPeriod(object): trade_day_mask = returns.index.normalize().isin(trading_days) - mask = ((returns.index >= self.start_date) & - (returns.index <= self.end_date) & trade_day_mask) + mask = ((returns.index >= self._start_session) & + (returns.index <= self._end_session) & trade_day_mask) returns = returns[mask] return returns diff --git a/zipline/finance/risk/report.py b/zipline/finance/risk/report.py index a54aff15..b01388d4 100644 --- a/zipline/finance/risk/report.py +++ b/zipline/finance/risk/report.py @@ -67,7 +67,7 @@ log = logbook.Logger('Risk Report') class RiskReport(object): - def __init__(self, algorithm_returns, sim_params, trading_schedule, + def __init__(self, algorithm_returns, sim_params, trading_calendar, treasury_curves, benchmark_returns, algorithm_leverages=None): """ @@ -80,23 +80,30 @@ class RiskReport(object): self.algorithm_returns = algorithm_returns self.sim_params = sim_params - self.trading_schedule = trading_schedule + self.trading_calendar = trading_calendar self.treasury_curves = treasury_curves self.benchmark_returns = benchmark_returns self.algorithm_leverages = algorithm_leverages if len(self.algorithm_returns) == 0: - start_date = self.sim_params.period_start - end_date = self.sim_params.period_end + start_session = self.sim_params.start_session + end_session = self.sim_params.end_session else: - start_date = self.algorithm_returns.index[0] - end_date = self.algorithm_returns.index[-1] + start_session = self.algorithm_returns.index[0] + end_session = self.algorithm_returns.index[-1] - self.month_periods = self.periods_in_range(1, start_date, end_date) - self.three_month_periods = self.periods_in_range(3, start_date, - end_date) - self.six_month_periods = self.periods_in_range(6, start_date, end_date) - self.year_periods = self.periods_in_range(12, start_date, end_date) + self.month_periods = self.periods_in_range( + 1, start_session, end_session + ) + self.three_month_periods = self.periods_in_range( + 3, start_session, end_session + ) + self.six_month_periods = self.periods_in_range( + 6, start_session, end_session + ) + self.year_periods = self.periods_in_range( + 12, start_session, end_session + ) def to_dict(self): """ @@ -120,10 +127,10 @@ class RiskReport(object): 'twelve_month': [x.to_dict() for x in self.year_periods], } - def periods_in_range(self, months_per, start, end): + def periods_in_range(self, months_per, start_session, end_session): one_day = datetime.timedelta(days=1) ends = [] - cur_start = start.replace(day=1) + cur_start = start_session.replace(day=1) # in edge cases (all sids filtered out, start/end are adjacent) # a test will not generate any returns data @@ -132,17 +139,18 @@ class RiskReport(object): # ensure that we have an end at the end of a calendar month, in case # the return series ends mid-month... - the_end = end.replace(day=1) + relativedelta(months=1) - one_day + the_end = end_session.replace(day=1) + relativedelta(months=1) - \ + one_day while True: cur_end = cur_start + relativedelta(months=months_per) - one_day - if(cur_end > the_end): + if cur_end > the_end: break cur_period_metrics = RiskMetricsPeriod( - start_date=cur_start, - end_date=cur_end, + start_session=cur_start, + end_session=cur_end, returns=self.algorithm_returns, benchmark_returns=self.benchmark_returns, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, treasury_curves=self.treasury_curves, algorithm_leverages=self.algorithm_leverages, ) diff --git a/zipline/finance/risk/risk.py b/zipline/finance/risk/risk.py index 19117df7..c34655a5 100644 --- a/zipline/finance/risk/risk.py +++ b/zipline/finance/risk/risk.py @@ -228,8 +228,8 @@ def select_treasury_duration(start_date, end_date): return treasury_duration -def choose_treasury(select_treasury, treasury_curves, start_date, end_date, - trading_schedule, compound=True): +def choose_treasury(select_treasury, treasury_curves, start_session, + end_session, trading_calendar, compound=True): """ Find the latest known interest rate for a given duration within a date range. @@ -237,48 +237,47 @@ def choose_treasury(select_treasury, treasury_curves, start_date, end_date, If we find one but it's more than a trading day ago from the date we're looking for, then we log a warning """ - treasury_duration = select_treasury(start_date, end_date) - end_day = end_date.replace(hour=0, minute=0, second=0, microsecond=0) + treasury_duration = select_treasury(start_session, end_session) search_day = None - if end_day in treasury_curves.index: + if end_session in treasury_curves.index: rate = get_treasury_rate(treasury_curves, treasury_duration, - end_day) + end_session) if rate is not None: - search_day = end_day + search_day = end_session if not search_day: # in case end date is not a trading day or there is no treasury # data, search for the previous day with an interest rate. search_days = treasury_curves.index - # Find rightmost value less than or equal to end_day - i = search_days.searchsorted(end_day) + # Find rightmost value less than or equal to end_session + i = search_days.searchsorted(end_session) for prev_day in search_days[i - 1::-1]: rate = get_treasury_rate(treasury_curves, treasury_duration, prev_day) if rate is not None: search_day = prev_day - search_dist = trading_schedule.execution_day_distance( - end_date, prev_day + search_dist = trading_calendar.session_distance( + end_session, prev_day ) break if search_day: if (search_dist is None or search_dist > 1) and \ - search_days[0] <= end_day <= search_days[-1]: + search_days[0] <= end_session <= search_days[-1]: message = "No rate within 1 trading day of end date = \ {dt} and term = {term}. Using {search_day}. Check that date doesn't exceed \ treasury history range." - message = message.format(dt=end_date, + message = message.format(dt=end_session, term=treasury_duration, search_day=search_day) log.warn(message) if search_day: - td = end_date - start_date + td = end_session - start_session if compound: return rate * (td.days + 1) / 365 else: @@ -287,7 +286,7 @@ treasury history range." message = "No rate for end date = {dt} and term = {term}. Check \ that date doesn't exceed treasury history range." message = message.format( - dt=end_date, + dt=end_session, term=treasury_duration ) raise Exception(message) diff --git a/zipline/finance/trading.py b/zipline/finance/trading.py index fe619133..79fe5b75 100644 --- a/zipline/finance/trading.py +++ b/zipline/finance/trading.py @@ -1,5 +1,5 @@ # -# Copyright 2015 Quantopian, Inc. +# Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,14 +14,15 @@ # limitations under the License. import logbook -import datetime - +import pandas as pd +from pandas.tslib import normalize_date from six import string_types from sqlalchemy import create_engine from zipline.assets import AssetDBWriter, AssetFinder from zipline.data.loader import load_market_data -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar +from zipline.utils.memoize import remember_last log = logbook.Logger('Trading') @@ -78,7 +79,7 @@ class TradingEnvironment(object): load=None, bm_symbol='^GSPC', exchange_tz="US/Eastern", - trading_schedule=default_nyse_schedule, + trading_calendar=None, asset_db_path=':memory:' ): @@ -86,9 +87,12 @@ class TradingEnvironment(object): if not load: load = load_market_data + if not trading_calendar: + trading_calendar = get_calendar("NYSE") + self.benchmark_returns, self.treasury_curves = load( - trading_schedule.day, - trading_schedule.schedule.index, + trading_calendar.day, + trading_calendar.schedule.index, self.bm_symbol, ) @@ -118,86 +122,134 @@ class TradingEnvironment(object): class SimulationParameters(object): - def __init__(self, period_start, period_end, + def __init__(self, start_session, end_session, + trading_calendar, capital_base=10e3, emission_rate='daily', data_frequency='daily', - trading_schedule=None, arena='backtest'): - self.period_start = period_start - self.period_end = period_end - self.capital_base = capital_base + assert type(start_session) == pd.Timestamp + assert type(end_session) == pd.Timestamp - self.emission_rate = emission_rate - self.data_frequency = data_frequency - - # copied to algorithm's environment for runtime access - self.arena = arena - - if trading_schedule is not None: - self.update_internal_from_trading_schedule( - trading_schedule=trading_schedule - ) - - def update_internal_from_trading_schedule(self, trading_schedule): - - assert self.period_start <= self.period_end, \ + assert trading_calendar is not None, \ + "Must pass in trading calendar!" + assert start_session <= end_session, \ "Period start falls after period end." - - assert self.period_start <= trading_schedule.last_execution_day, \ + assert start_session <= trading_calendar.last_trading_session, \ "Period start falls after the last known trading day." - assert self.period_end >= trading_schedule.first_execution_day, \ + assert end_session >= trading_calendar.first_trading_session, \ "Period end falls before the first known trading day." - self.first_open = self._calculate_first_open(trading_schedule) - self.last_close = self._calculate_last_close(trading_schedule) + # chop off any minutes or hours on the given start and end dates, + # as we only support session labels here (and we represent session + # labels as midnight UTC). + self._start_session = normalize_date(start_session) + self._end_session = normalize_date(end_session) + self._capital_base = capital_base - # Take the length of an inclusive slice of trading dates - self.trading_days = trading_schedule.trading_dates( - self.first_open, self.last_close + self._emission_rate = emission_rate + self._data_frequency = data_frequency + + # copied to algorithm's environment for runtime access + self._arena = arena + + self._trading_calendar = trading_calendar + + if not trading_calendar.is_session(self._start_session): + # if the start date is not a valid session in this calendar, + # push it forward to the first valid session + self._start_session = trading_calendar.minute_to_session_label( + self._start_session + ) + + if not trading_calendar.is_session(self._end_session): + # if the end date is not a valid session in this calendar, + # pull it backward to the last valid session before the given + # end date. + self._end_session = trading_calendar.minute_to_session_label( + self._end_session, direction="previous" + ) + + self._first_open = trading_calendar.open_and_close_for_session( + self._start_session + )[0] + self._last_close = trading_calendar.open_and_close_for_session( + self._end_session + )[1] + + @property + def capital_base(self): + return self._capital_base + + @property + def emission_rate(self): + return self._emission_rate + + @property + def data_frequency(self): + return self._data_frequency + + @data_frequency.setter + def data_frequency(self, val): + self._data_frequency = val + + @property + def arena(self): + return self._arena + + @arena.setter + def arena(self, val): + self._arena = val + + @property + def start_session(self): + return self._start_session + + @property + def end_session(self): + return self._end_session + + @property + def first_open(self): + return self._first_open + + @property + def last_close(self): + return self._last_close + + @property + @remember_last + def sessions(self): + return self._trading_calendar.sessions_in_range( + self.start_session, + self.end_session ) - self.days_in_period = len(self.trading_days) - def _calculate_first_open(self, trading_schedule): - """ - Finds the first trading day on or after self.period_start. - """ - first_open = self.period_start - one_day = datetime.timedelta(days=1) - - while not trading_schedule.is_executing_on_day(first_open): - first_open = first_open + one_day - - mkt_open, _ = trading_schedule.start_and_end(first_open) - return mkt_open - - def _calculate_last_close(self, trading_schedule): - """ - Finds the last trading day on or before self.period_end - """ - last_close = self.period_end - one_day = datetime.timedelta(days=1) - - while not trading_schedule.is_executing_on_day(last_close): - last_close = last_close - one_day - - _, mkt_close = trading_schedule.start_and_end(last_close) - return mkt_close + def create_new(self, start_session, end_session): + return SimulationParameters( + start_session, + end_session, + self._trading_calendar, + capital_base=self.capital_base, + emission_rate=self.emission_rate, + data_frequency=self.data_frequency, + arena=self.arena + ) def __repr__(self): return """ {class_name}( - period_start={period_start}, - period_end={period_end}, + start_session={start_session}, + end_session={end_session}, capital_base={capital_base}, data_frequency={data_frequency}, emission_rate={emission_rate}, first_open={first_open}, last_close={last_close})\ """.format(class_name=self.__class__.__name__, - period_start=self.period_start, - period_end=self.period_end, + start_session=self.start_session, + end_session=self.end_session, capital_base=self.capital_base, data_frequency=self.data_frequency, emission_rate=self.emission_rate, diff --git a/zipline/gens/tradesimulation.py b/zipline/gens/tradesimulation.py index bcc23063..39bb0a95 100644 --- a/zipline/gens/tradesimulation.py +++ b/zipline/gens/tradesimulation.py @@ -215,9 +215,7 @@ class AlgorithmSimulator(object): # perspective as we have technically not "advanced" to the # current dt yet. algo.perf_tracker.position_tracker.sync_last_sale_prices( - self.algo.trading_schedule.previous_execution_minute( - dt - ), + self.algo.trading_calendar.previous_minute(dt), False, self.data_portal ) diff --git a/zipline/pipeline/loaders/equity_pricing_loader.py b/zipline/pipeline/loaders/equity_pricing_loader.py index c8be9ae1..aba34f20 100644 --- a/zipline/pipeline/loaders/equity_pricing_loader.py +++ b/zipline/pipeline/loaders/equity_pricing_loader.py @@ -22,7 +22,7 @@ from zipline.data.us_equity_pricing import ( ) from zipline.lib.adjusted_array import AdjustedArray from zipline.errors import NoFurtherDataError -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar from .base import PipelineLoader @@ -40,7 +40,7 @@ class USEquityPricingLoader(PipelineLoader): self.raw_price_loader = raw_price_loader self.adjustments_loader = adjustments_loader - self._calendar = default_nyse_schedule.all_execution_days + self._calendar = get_calendar("NYSE").all_sessions @classmethod def from_files(cls, pricing_path, adjustments_path): diff --git a/zipline/sources/benchmark_source.py b/zipline/sources/benchmark_source.py index beafe3dd..b1816bcf 100644 --- a/zipline/sources/benchmark_source.py +++ b/zipline/sources/benchmark_source.py @@ -23,15 +23,15 @@ from zipline.errors import ( class BenchmarkSource(object): - def __init__(self, benchmark_sid, env, trading_schedule, trading_days, + def __init__(self, benchmark_sid, env, trading_calendar, sessions, data_portal, emission_rate="daily"): self.benchmark_sid = benchmark_sid self.env = env - self.trading_days = trading_days + self.sessions = sessions self.emission_rate = emission_rate self.data_portal = data_portal - if len(trading_days) == 0: + if len(sessions) == 0: self._precalculated_series = pd.Series() elif self.benchmark_sid: benchmark_asset = self.env.asset_finder.retrieve_asset( @@ -42,22 +42,22 @@ class BenchmarkSource(object): self._precalculated_series = \ self._initialize_precalculated_series( benchmark_asset, - trading_schedule, - self.trading_days, + trading_calendar, + self.sessions, self.data_portal ) else: # get benchmark info from trading environment, which defaults to # downloading data from Yahoo. daily_series = \ - env.benchmark_returns[trading_days[0]:trading_days[-1]] + env.benchmark_returns[sessions[0]:sessions[-1]] if self.emission_rate == "minute": # we need to take the env's benchmark returns, which are daily, # and resample them to minute - minutes = trading_schedule.execution_minutes_for_days_in_range( - start=trading_days[0], - end=trading_days[-1] + minutes = trading_calendar.minutes_for_sessions_in_range( + sessions[0], + sessions[-1] ) minute_series = daily_series.reindex( @@ -78,7 +78,7 @@ class BenchmarkSource(object): # as benchmark. stock_dividends = \ self.data_portal.get_stock_dividends(self.benchmark_sid, - self.trading_days) + self.sessions) if len(stock_dividends) > 0: raise InvalidBenchmarkAsset( @@ -86,23 +86,23 @@ class BenchmarkSource(object): dt=stock_dividends[0]["ex_date"] ) - if benchmark_asset.start_date > self.trading_days[0]: + if benchmark_asset.start_date > self.sessions[0]: # the asset started trading after the first simulation day raise BenchmarkAssetNotAvailableTooEarly( sid=str(self.benchmark_sid), - dt=self.trading_days[0], + dt=self.sessions[0], start_dt=benchmark_asset.start_date ) - if benchmark_asset.end_date < self.trading_days[-1]: + if benchmark_asset.end_date < self.sessions[-1]: # the asset stopped trading before the last simulation day raise BenchmarkAssetNotAvailableTooLate( sid=str(self.benchmark_sid), - dt=self.trading_days[-1], + dt=self.sessions[-1], end_dt=benchmark_asset.end_date ) - def _initialize_precalculated_series(self, asset, trading_schedule, + def _initialize_precalculated_series(self, asset, trading_calendar, trading_days, data_portal): """ Internal method that pre-calculates the benchmark return series for @@ -112,7 +112,7 @@ class BenchmarkSource(object): ---------- asset: Asset to use - trading_schedule: TradingSchedule + trading_calendar: TradingCalendar trading_days: pd.DateTimeIndex @@ -137,8 +137,8 @@ class BenchmarkSource(object): change from close to close. """ if self.emission_rate == "minute": - minutes = trading_schedule.execution_minutes_for_days_in_range( - self.trading_days[0], self.trading_days[-1] + minutes = trading_calendar.minutes_for_sessions_in_range( + self.sessions[0], self.sessions[-1] ) benchmark_series = data_portal.get_history_window( [asset], diff --git a/zipline/sources/test_source.py b/zipline/sources/test_source.py index cbbe5494..3934dd49 100644 --- a/zipline/sources/test_source.py +++ b/zipline/sources/test_source.py @@ -52,7 +52,7 @@ def create_trade(sid, price, amount, datetime, source_id="test_factory"): def date_gen(start, end, - trading_schedule, + trading_calendar, delta=timedelta(minutes=1), repeats=None): """ @@ -73,15 +73,19 @@ def date_gen(start, """ cur = cur + delta - if not (trading_schedule.is_executing_on_day - if daily_delta - else trading_schedule.is_executing_on_minute)(cur): - if daily_delta: - return trading_schedule.next_execution_day(cur) - else: - return trading_schedule.next_start_and_end(cur)[0] - else: + currently_executing = \ + (daily_delta and (cur in trading_calendar.all_sessions)) or \ + (trading_calendar.is_open_on_minute(cur)) + + if currently_executing: return cur + else: + if daily_delta: + return trading_calendar.minute_to_session_label(cur) + else: + return trading_calendar.open_and_close_for_session( + trading_calendar.minute_to_session_label(cur) + )[0] # yield count trade events, all on trading days, and # during trading hours. @@ -109,12 +113,12 @@ class SpecificEquityTrades(object): delta : timedelta between internal events filter : filter to remove the sids """ - def __init__(self, env, trading_schedule, *args, **kwargs): + def __init__(self, env, trading_calendar, *args, **kwargs): # We shouldn't get any positional arguments. assert len(args) == 0 self.env = env - self.trading_schedule = trading_schedule + self.trading_calendar = trading_calendar # Default to None for event_list and filter. self.event_list = kwargs.get('event_list') @@ -206,14 +210,14 @@ class SpecificEquityTrades(object): end=self.end, delta=self.delta, repeats=len(self.sids), - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) else: date_generator = date_gen( start=self.start, end=self.end, delta=self.delta, - trading_schedule=self.trading_schedule, + trading_calendar=self.trading_calendar, ) source_id = self.get_hash() diff --git a/zipline/testing/core.py b/zipline/testing/core.py index 4897f85a..82ef63d3 100644 --- a/zipline/testing/core.py +++ b/zipline/testing/core.py @@ -49,7 +49,7 @@ from zipline.pipeline.loaders.testing import make_seeded_random_loader from zipline.utils import security_list from zipline.utils.input_validation import expect_dimensions from zipline.utils.sentinel import sentinel -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar import numpy as np from numpy import float64 @@ -410,10 +410,19 @@ class ExplodingObject(object): raise UnexpectedAttributeAccess(name) -def write_minute_data(trading_schedule, tempdir, minutes, sids): +def write_minute_data(trading_calendar, tempdir, minutes, sids): + first_session = trading_calendar.minute_to_session_label( + minutes[0], direction="none" + ) + last_session = trading_calendar.minute_to_session_label( + minutes[-1], direction="none" + ) + + sessions = trading_calendar.sessions_in_range(first_session, last_session) + write_bcolz_minute_data( - trading_schedule, - trading_schedule.execution_days_in_range(minutes[0], minutes[-1]), + trading_calendar, + sessions, tempdir.path, create_minute_bar_data(minutes, sids), ) @@ -435,8 +444,8 @@ def create_minute_bar_data(minutes, sids): ) -def create_daily_bar_data(trading_days, sids): - length = len(trading_days) +def create_daily_bar_data(sessions, sids): + length = len(sessions) for sid_idx, sid in enumerate(sids): yield sid, pd.DataFrame( { @@ -445,56 +454,57 @@ def create_daily_bar_data(trading_days, sids): "low": (np.array(range(8, 8 + length)) + sid_idx), "close": (np.array(range(10, 10 + length)) + sid_idx), "volume": np.array(range(100, 100 + length)) + sid_idx, - "day": [day.value for day in trading_days] + "day": [session.value for session in sessions] }, - index=trading_days, + index=sessions, ) -def write_daily_data(tempdir, sim_params, sids): +def write_daily_data(tempdir, sim_params, sids, trading_calendar): path = os.path.join(tempdir.path, "testdaily.bcolz") - BcolzDailyBarWriter(path, sim_params.trading_days).write( - create_daily_bar_data(sim_params.trading_days, sids), + BcolzDailyBarWriter(path, sim_params.sessions, trading_calendar).write( + create_daily_bar_data(sim_params.sessions, sids), ) return path def create_data_portal(asset_finder, tempdir, sim_params, sids, - trading_schedule, adjustment_reader=None): + trading_calendar, adjustment_reader=None): if sim_params.data_frequency == "daily": - daily_path = write_daily_data(tempdir, sim_params, sids) + daily_path = write_daily_data(tempdir, sim_params, sids, + trading_calendar) equity_daily_reader = BcolzDailyBarReader(daily_path) return DataPortal( - asset_finder, trading_schedule, + asset_finder, trading_calendar, first_trading_day=equity_daily_reader.first_trading_day, equity_daily_reader=equity_daily_reader, adjustment_reader=adjustment_reader ) else: - minutes = trading_schedule.execution_minutes_for_days_in_range( + minutes = trading_calendar.minutes_in_range( sim_params.first_open, sim_params.last_close ) - minute_path = write_minute_data(trading_schedule, tempdir, minutes, + minute_path = write_minute_data(trading_calendar, tempdir, minutes, sids) equity_minute_reader = BcolzMinuteBarReader(minute_path) return DataPortal( - asset_finder, trading_schedule, + asset_finder, trading_calendar, first_trading_day=equity_minute_reader.first_trading_day, equity_minute_reader=equity_minute_reader, adjustment_reader=adjustment_reader ) -def write_bcolz_minute_data(trading_schedule, days, path, data): - market_opens = trading_schedule.schedule.loc[days].market_open - market_closes = trading_schedule.schedule.loc[days].market_close +def write_bcolz_minute_data(trading_calendar, days, path, data): + market_opens = trading_calendar.schedule.loc[days].market_open + market_closes = trading_calendar.schedule.loc[days].market_close BcolzMinuteBarWriter( days[0], @@ -505,14 +515,14 @@ def write_bcolz_minute_data(trading_schedule, days, path, data): ).write(data) -def create_minute_df_for_asset(trading_schedule, +def create_minute_df_for_asset(trading_calendar, start_dt, end_dt, interval=1, start_val=1, minute_blacklist=None): - asset_minutes = trading_schedule.execution_minutes_for_days_in_range( + asset_minutes = trading_calendar.minutes_for_sessions_in_range( start_dt, end_dt ) minutes_count = len(asset_minutes) @@ -542,9 +552,9 @@ def create_minute_df_for_asset(trading_schedule, return df -def create_daily_df_for_asset(trading_schedule, start_day, end_day, +def create_daily_df_for_asset(trading_calendar, start_day, end_day, interval=1): - days = trading_schedule.execution_days_in_range(start_day, end_day) + days = trading_calendar.minutes_in_range(start_day, end_day) days_count = len(days) days_arr = np.arange(days_count) + 2 @@ -598,23 +608,23 @@ def trades_by_sid_to_dfs(trades_by_sid, index): ) -def create_data_portal_from_trade_history(asset_finder, trading_schedule, +def create_data_portal_from_trade_history(asset_finder, trading_calendar, tempdir, sim_params, trades_by_sid): if sim_params.data_frequency == "daily": path = os.path.join(tempdir.path, "testdaily.bcolz") - BcolzDailyBarWriter(path, sim_params.trading_days).write( - trades_by_sid_to_dfs(trades_by_sid, sim_params.trading_days), + BcolzDailyBarWriter(path, sim_params.sessions, trading_calendar).write( + trades_by_sid_to_dfs(trades_by_sid, sim_params.sessions), ) equity_daily_reader = BcolzDailyBarReader(path) return DataPortal( - asset_finder, trading_schedule, + asset_finder, trading_calendar, first_trading_day=equity_daily_reader.first_trading_day, equity_daily_reader=equity_daily_reader, ) else: - minutes = trading_schedule.execution_minutes_for_days_in_range( + minutes = trading_calendar.minutes_in_range( sim_params.first_open, sim_params.last_close ) @@ -649,11 +659,8 @@ def create_data_portal_from_trade_history(asset_finder, trading_schedule, }).set_index("dt") write_bcolz_minute_data( - trading_schedule, - trading_schedule.execution_days_in_range( - sim_params.first_open, - sim_params.last_close - ), + trading_calendar, + sim_params.sessions, tempdir.path, assets ) @@ -661,21 +668,23 @@ def create_data_portal_from_trade_history(asset_finder, trading_schedule, equity_minute_reader = BcolzMinuteBarReader(tempdir.path) return DataPortal( - asset_finder, trading_schedule, + asset_finder, trading_calendar, first_trading_day=equity_minute_reader.first_trading_day, equity_minute_reader=equity_minute_reader, ) class FakeDataPortal(DataPortal): - - def __init__(self, env=None, trading_schedule=default_nyse_schedule, + def __init__(self, env=None, trading_calendar=None, first_trading_day=None): if env is None: env = TradingEnvironment() + if trading_calendar is None: + trading_calendar = get_calendar("NYSE") + super(FakeDataPortal, self).__init__(env.asset_finder, - trading_schedule, + trading_calendar, first_trading_day) def get_spot_value(self, asset, field, dt, data_frequency): @@ -688,8 +697,8 @@ class FakeDataPortal(DataPortal): ffill=True): if frequency == "1d": end_idx = \ - self.trading_schedule.all_execution_days.searchsorted(end_dt) - days = self.trading_schedule.all_execution_days[ + self.trading_calendar.all_sessions.searchsorted(end_dt) + days = self.trading_calendar.all_sessions[ (end_idx - bar_count + 1):(end_idx + 1) ] @@ -707,8 +716,8 @@ class FetcherDataPortal(DataPortal): Mock dataportal that returns fake data for history and non-fetcher spot value. """ - def __init__(self, asset_finder, trading_schedule, first_trading_day=None): - super(FetcherDataPortal, self).__init__(asset_finder, trading_schedule, + def __init__(self, asset_finder, trading_calendar, first_trading_day=None): + super(FetcherDataPortal, self).__init__(asset_finder, trading_calendar, first_trading_day) def get_spot_value(self, asset, field, dt, data_frequency): @@ -1023,7 +1032,7 @@ def gen_calendars(start, stop, critical_dates): yield (all_dates.drop(to_drop),) # Also test with the trading calendar. - trading_days = default_nyse_schedule.all_execution_days + trading_days = get_calendar("NYSE").all_days yield (trading_days[trading_days.slice_indexer(start, stop)],) diff --git a/zipline/testing/fixtures.py b/zipline/testing/fixtures.py index f657cbf3..76a60844 100644 --- a/zipline/testing/fixtures.py +++ b/zipline/testing/fixtures.py @@ -37,7 +37,6 @@ from zipline.pipeline import SimplePipelineEngine from zipline.pipeline.loaders.testing import make_seeded_random_loader from zipline.utils.calendars import ( get_calendar, - ExchangeTradingSchedule, ) @@ -364,41 +363,28 @@ class WithAssetFinder(WithDefaultDateBounds): cls.asset_finder = cls.make_asset_finder() -class WithTradingSchedule(object): +class WithTradingCalendar(object): """ - ZiplineTestCase mixing providing cls.trading_schedule as a class-level + ZiplineTestCase mixing providing cls.trading_calendar as a class-level fixture. - After ``init_class_fixtures`` has been called, `cls.trading_schedule` is - populated with a trading schedule. + After ``init_class_fixtures`` has been called, `cls.trading_calendar` is + populated with a trading calendar. Attributes ---------- - TRADING_SCHEDULE_CALENDAR : ExchangeCalendar - The ExchangeCalendar to be wrapped in an ExchangeTradingSchedule. - - Methods - ------- - make_trading_schedule() -> TradingSchedule - A class method that constructs the trading schedule for the class. - - See Also - -------- - :class:`zipline.utils.calendars.trading_schedule.TradingSchedule` + TRADING_CALENDAR_STR : str + The identifier of the calendar to use. """ - TRADING_SCHEDULE_CALENDAR = get_calendar('NYSE') - - @classmethod - def make_trading_schedule(cls): - return ExchangeTradingSchedule(cls.TRADING_SCHEDULE_CALENDAR) + TRADING_CALENDAR_STR = 'NYSE' @classmethod def init_class_fixtures(cls): - super(WithTradingSchedule, cls).init_class_fixtures() - cls.trading_schedule = cls.make_trading_schedule() + super(WithTradingCalendar, cls).init_class_fixtures() + cls.trading_calendar = get_calendar(cls.TRADING_CALENDAR_STR) -class WithTradingEnvironment(WithAssetFinder, WithTradingSchedule): +class WithTradingEnvironment(WithAssetFinder, WithTradingCalendar): """ ZiplineTestCase mixin providing cls.env as a class-level fixture. @@ -441,7 +427,7 @@ class WithTradingEnvironment(WithAssetFinder, WithTradingSchedule): return TradingEnvironment( load=cls.make_load_function(), asset_db_path=cls.asset_finder.engine, - trading_schedule=cls.trading_schedule, + trading_calendar=cls.trading_calendar, ) @classmethod @@ -496,7 +482,7 @@ class WithSimParams(WithTradingEnvironment): capital_base=cls.SIM_PARAMS_CAPITAL_BASE, data_frequency=cls.SIM_PARAMS_DATA_FREQUENCY, emission_rate=cls.SIM_PARAMS_EMISSION_RATE, - trading_schedule=cls.trading_schedule, + trading_calendar=cls.trading_calendar, ) @classmethod @@ -505,7 +491,7 @@ class WithSimParams(WithTradingEnvironment): cls.sim_params = cls.make_simparams() -class WithNYSETradingDays(WithTradingSchedule): +class WithNYSETradingDays(WithTradingCalendar): """ ZiplineTestCase mixin providing cls.trading_days as a class-level fixture. @@ -530,7 +516,7 @@ class WithNYSETradingDays(WithTradingSchedule): def init_class_fixtures(cls): super(WithNYSETradingDays, cls).init_class_fixtures() - all_days = cls.trading_schedule.all_execution_days + all_days = cls.trading_calendar.all_sessions start_loc = all_days.get_loc(cls.DATA_MIN_DAY, 'bfill') end_loc = all_days.get_loc(cls.DATA_MAX_DAY, 'ffill') @@ -614,6 +600,7 @@ class WithEquityDailyBarData(WithTradingEnvironment): zipline.testing.create_daily_bar_data """ EQUITY_DAILY_BAR_LOOKBACK_DAYS = 0 + EQUITY_DAILY_BAR_USE_FULL_CALENDAR = False EQUITY_DAILY_BAR_START_DATE = alias('START_DATE') EQUITY_DAILY_BAR_END_DATE = alias('END_DATE') @@ -634,9 +621,9 @@ class WithEquityDailyBarData(WithTradingEnvironment): # source from minute logic. 'volume': 'last' } - mm = cls.trading_schedule.all_execution_minutes - m_opens = cls.trading_schedule.schedule.market_open - m_closes = cls.trading_schedule.schedule.market_close + mm = cls.trading_calendar.all_minutes + m_opens = cls.trading_calendar.schedule.market_open + m_closes = cls.trading_calendar.schedule.market_close minute_data = dict(cls.make_equity_minute_bar_data()) @@ -667,15 +654,28 @@ class WithEquityDailyBarData(WithTradingEnvironment): def init_class_fixtures(cls): super(WithEquityDailyBarData, cls).init_class_fixtures() if cls.EQUITY_DAILY_BAR_USE_FULL_CALENDAR: - days = cls.trading_schedule.all_execution_days + days = cls.trading_calendar.all_sessions else: - days = cls.trading_schedule.execution_days_in_range( - cls.trading_schedule.add_execution_days( - -1 * cls.EQUITY_DAILY_BAR_LOOKBACK_DAYS, - cls.EQUITY_DAILY_BAR_START_DATE, - ), + if cls.trading_calendar.is_session( + cls.EQUITY_DAILY_BAR_START_DATE + ): + first_session = cls.EQUITY_DAILY_BAR_START_DATE + else: + first_session = cls.trading_calendar.minute_to_session_label( + pd.Timestamp(cls.EQUITY_DAILY_BAR_START_DATE) + ) + + if cls.EQUITY_DAILY_BAR_LOOKBACK_DAYS > 0: + first_session = cls.trading_calendar.sessions_window( + first_session, + -1 * cls.EQUITY_DAILY_BAR_LOOKBACK_DAYS + )[0] + + days = cls.trading_calendar.sessions_in_range( + first_session, cls.EQUITY_DAILY_BAR_END_DATE, ) + cls.equity_daily_bar_days = days @@ -746,7 +746,7 @@ class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir): days = cls.equity_daily_bar_days cls.bcolz_daily_bar_ctable = t = getattr( - BcolzDailyBarWriter(p, days), + BcolzDailyBarWriter(p, days, cls.trading_calendar), cls._write_method_name, )(cls.make_equity_daily_bar_data()) @@ -813,7 +813,7 @@ class WithEquityMinuteBarData(WithTradingEnvironment): @classmethod def make_equity_minute_bar_data(cls): return create_minute_bar_data( - cls.trading_schedule.execution_minutes_for_days_in_range( + cls.trading_calendar.minutes_for_sessions_in_range( cls.equity_minute_bar_days[0], cls.equity_minute_bar_days[-1], ), @@ -824,15 +824,23 @@ class WithEquityMinuteBarData(WithTradingEnvironment): def init_class_fixtures(cls): super(WithEquityMinuteBarData, cls).init_class_fixtures() if cls.EQUITY_MINUTE_BAR_USE_FULL_CALENDAR: - days = cls.trading_schedule.all_execution_days + days = cls.trading_calendar.all_execution_days else: - days = cls.trading_schedule.execution_days_in_range( - cls.trading_schedule.add_execution_days( - -1 * cls.EQUITY_MINUTE_BAR_LOOKBACK_DAYS, - cls.EQUITY_MINUTE_BAR_START_DATE, - ), - cls.EQUITY_MINUTE_BAR_END_DATE, + first_session = cls.trading_calendar.minute_to_session_label( + pd.Timestamp(cls.EQUITY_MINUTE_BAR_START_DATE) ) + + if cls.EQUITY_MINUTE_BAR_LOOKBACK_DAYS > 0: + first_session = cls.trading_calendar.sessions_window( + first_session, + -1 * cls.EQUITY_MINUTE_BAR_LOOKBACK_DAYS + )[0] + + days = cls.trading_calendar.sessions_in_range( + first_session, + cls.EQUITY_MINUTE_BAR_END_DATE + ) + cls.equity_minute_bar_days = days @@ -889,11 +897,12 @@ class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir): cls.bcolz_minute_bar_path = p = \ cls.make_bcolz_minute_bar_rootdir_path() days = cls.equity_minute_bar_days + writer = BcolzMinuteBarWriter( days[0], p, - cls.trading_schedule.schedule.market_open.loc[days], - cls.trading_schedule.schedule.market_close.loc[days], + cls.trading_calendar.schedule.market_open.loc[days], + cls.trading_calendar.schedule.market_close.loc[days], US_EQUITIES_MINUTES_PER_DAY ) writer.write(cls.make_equity_minute_bar_data()) @@ -1108,7 +1117,7 @@ class WithDataPortal(WithAdjustmentReader, return DataPortal( self.env.asset_finder, - self.trading_schedule, + self.trading_calendar, first_trading_day=self.DATA_PORTAL_FIRST_TRADING_DAY, equity_daily_reader=( self.bcolz_equity_daily_bar_reader diff --git a/zipline/utils/calendars/__init__.py b/zipline/utils/calendars/__init__.py index c4d74f24..49bcc4dc 100644 --- a/zipline/utils/calendars/__init__.py +++ b/zipline/utils/calendars/__init__.py @@ -13,14 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .exchange_calendar import ( - ExchangeCalendar, get_calendar +from .trading_calendar import TradingCalendar +from .calendar_utils import ( + get_calendar, + register_calendar, + deregister_calendar, + clear_calendars ) -from .trading_schedule import ( - TradingSchedule, ExchangeTradingSchedule, default_nyse_schedule -) -from .calendar_helpers import normalize_date -__all__ = ['get_calendar', 'ExchangeCalendar', 'TradingSchedule', - 'ExchangeTradingSchedule', 'default_nyse_schedule', - 'normalize_date'] +__all__ = ['get_calendar', 'TradingCalendar', 'register_calendar', + 'deregister_calendar', 'clear_calendars'] diff --git a/zipline/utils/calendars/_calendar_helpers.pyx b/zipline/utils/calendars/_calendar_helpers.pyx new file mode 100644 index 00000000..0f7e0520 --- /dev/null +++ b/zipline/utils/calendars/_calendar_helpers.pyx @@ -0,0 +1,53 @@ +from numpy cimport ndarray, long_t +from numpy import searchsorted +cimport cython + + +@cython.boundscheck(False) +@cython.wraparound(False) +def next_divider_idx(ndarray[long_t, ndim=1] dividers, long_t minute_val): + cdef int divider_idx + cdef long target + + divider_idx = searchsorted(dividers, minute_val, side="right") + target = dividers[divider_idx] + + if minute_val == target: + # if dt is exactly on the divider, go to the next value + return divider_idx + 1 + else: + return divider_idx + +@cython.boundscheck(False) +@cython.wraparound(False) +def previous_divider_idx(ndarray[long_t, ndim=1] dividers, + long_t minute_val): + cdef int divider_idx + + divider_idx = searchsorted(dividers, minute_val) + + if divider_idx == 0: + raise ValueError("Cannot go earlier in calendar!") + + return divider_idx - 1 + +def is_open(ndarray[long_t, ndim=1] opens, + ndarray[long_t, ndim=1] closes, + long_t minute_val): + cdef open_idx, close_idx + + open_idx = searchsorted(opens, minute_val) + close_idx = searchsorted(closes, minute_val) + + if open_idx != close_idx: + # if the indices are not same, that means the market is open + return True + else: + try: + # if they are the same, it might be the first minute of a + # session + return minute_val == opens[open_idx] + except IndexError: + # this can happen if we're outside the schedule's range (like + # after the last close) + return False diff --git a/zipline/utils/calendars/calendar_helpers.py b/zipline/utils/calendars/calendar_helpers.py deleted file mode 100644 index f99c41d1..00000000 --- a/zipline/utils/calendars/calendar_helpers.py +++ /dev/null @@ -1,239 +0,0 @@ -# -# Copyright 2016 Quantopian, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pandas as pd -import numpy as np -import bisect - -from zipline.errors import NoFurtherDataError - - -def normalize_date(date): - date = pd.Timestamp(date, tz='UTC') - return pd.tseries.tools.normalize_date(date) - - -def delta_from_time(t): - """ - Convert a datetime.time into a timedelta. - """ - return pd.Timedelta( - hours=t.hour, - minutes=t.minute, - seconds=t.second, - ) - - -def _get_index(dt, all_trading_days): - """ - Return the index of the given @dt, or the index of the preceding - trading day if the given dt is not in the trading calendar. - """ - ndt = normalize_date(dt) - if ndt in all_trading_days: - return all_trading_days.searchsorted(ndt) - else: - return all_trading_days.searchsorted(ndt) - 1 - -# The following methods are intended to be inserted in both the -# ExchangeCalendar and TradingSchedule classes. -# These methods live in the helpers module to avoid code duplication. - - -def next_scheduled_day(date, last_trading_day, is_scheduled_day_hook): - """ - Returns the next session date in the calendar after the provided date. - - Parameters - ---------- - date : Timestamp - The date whose following date is needed. - - Returns - ------- - Timestamp - The next scheduled date after the provided date. - """ - dt = normalize_date(date) - delta = pd.Timedelta(days=1) - - while dt <= last_trading_day: - dt += delta - if is_scheduled_day_hook(dt): - return dt - raise NoFurtherDataError(msg='Cannot find next day after %s' % date) - - -def previous_scheduled_day(date, first_trading_day, is_scheduled_day_hook): - """ - Returns the previous session date in the calendar before the provided date. - - Parameters - ---------- - date : Timestamp - The date whose previous date is needed. - - Returns - ------- - Timestamp - The previous scheduled date before the provided date. - """ - dt = normalize_date(date) - delta = pd.Timedelta(days=-1) - - while first_trading_day < dt: - dt += delta - if is_scheduled_day_hook(dt): - return dt - raise NoFurtherDataError(msg='Cannot find previous day before %s' % date) - - -def next_open_and_close(date, open_and_close_hook, - next_scheduled_day_hook): - return open_and_close_hook(next_scheduled_day_hook(date)) - - -def previous_open_and_close(date, open_and_close_hook, - previous_scheduled_day_hook): - return open_and_close_hook(previous_scheduled_day_hook(date)) - - -def scheduled_day_distance(first_date, second_date, all_days): - first_date = normalize_date(first_date) - second_date = normalize_date(second_date) - - i = bisect.bisect_left(all_days, first_date) - if i == len(all_days): # nothing found - return None - j = bisect.bisect_left(all_days, second_date) - if j == len(all_days): - return None - distance = j - 1 - assert distance >= 0 - return distance - - -def minutes_for_day(day, open_and_close_hook): - start, end = open_and_close_hook(day) - return pd.date_range(start, end, freq='T') - - -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)] - - -def minutes_for_days_in_range(start, end, days_in_range_hook, - minutes_for_day_hook): - """ - Get all execution minutes for the days between start and end, - inclusive. - """ - start_date = normalize_date(start) - end_date = normalize_date(end) - - all_minutes = [] - for day in days_in_range_hook(start_date, end_date): - day_minutes = minutes_for_day_hook(day) - all_minutes.append(day_minutes) - - # Concatenate all minutes and truncate minutes before start/after end. - return pd.DatetimeIndex(np.concatenate(all_minutes), copy=False, tz='UTC') - - -def add_scheduled_days(n, date, next_scheduled_day_hook, - previous_scheduled_day_hook, all_trading_days): - """ - Adds n trading days to date. If this would fall outside of the - trading calendar, a NoFurtherDataError is raised. - - Parameters - ---------- - n : int - The number of days to add to date, this can be positive or - negative. - date : datetime - The date to add to. - - Returns - ------- - datetime - n trading days added to date. - """ - if n == 1: - return next_scheduled_day_hook(date) - if n == -1: - return previous_scheduled_day_hook(date) - - idx = _get_index(date, all_trading_days) + n - if idx < 0 or idx >= len(all_trading_days): - raise NoFurtherDataError( - msg='Cannot add %d days to %s' % (n, date) - ) - - return all_trading_days[idx] - - -def all_scheduled_minutes(all_days, minutes_for_days_in_range_hook): - first_day = all_days[0] - last_day = all_days[-1] - return minutes_for_days_in_range_hook(first_day, last_day) - - -def next_scheduled_minute(start, is_scheduled_day_hook, open_and_close_hook, - next_open_and_close_hook): - """ - Get the next market minute after @start. This is either the immediate - next minute, the open of the same day if @start is before the market - open on a trading day, or the open of the next market day after @start. - """ - if is_scheduled_day_hook(start): - market_open, market_close = open_and_close_hook(start) - # If start before market open on a trading day, return market open. - if start < market_open: - return market_open - # If start is during trading hours, then get the next minute. - elif start < market_close: - return start + pd.Timedelta(minutes=1) - # If start is not in a trading day, or is after the market close - # then return the open of the *next* trading day. - return next_open_and_close_hook(start)[0] - - -def previous_scheduled_minute(start, is_scheduled_day_hook, - open_and_close_hook, - previous_open_and_close_hook): - """ - Get the next market minute before @start. This is either the immediate - previous minute, the close of the same day if @start is after the close - on a trading day, or the close of the market day before @start. - """ - if is_scheduled_day_hook(start): - market_open, market_close = open_and_close_hook(start) - # If start after the market close, return market close. - if start > market_close: - return market_close - # If start is during trading hours, then get previous minute. - if start > market_open: - return start - pd.Timedelta(minutes=1) - # 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] diff --git a/zipline/utils/calendars/calendar_utils.py b/zipline/utils/calendars/calendar_utils.py new file mode 100644 index 00000000..cadd816b --- /dev/null +++ b/zipline/utils/calendars/calendar_utils.py @@ -0,0 +1,96 @@ +from zipline.errors import ( + InvalidCalendarName, + CalendarNameCollision, +) + +from zipline.utils.calendars.exchange_calendar_nyse import NYSEExchangeCalendar +from zipline.utils.calendars.exchange_calendar_cme import CMEExchangeCalendar +from zipline.utils.calendars.exchange_calendar_bmf import BMFExchangeCalendar +from zipline.utils.calendars.exchange_calendar_lse import LSEExchangeCalendar +from zipline.utils.calendars.exchange_calendar_tsx import TSXExchangeCalendar + +_static_calendars = {} + + +def get_calendar(name): + """ + Retrieves an instance of an TradingCalendar whose name is given. + + Parameters + ---------- + name : str + The name of the TradingCalendar to be retrieved. + + Returns + ------- + TradingCalendar + The desired calendar. + """ + if name not in _static_calendars: + if name == 'NYSE': + cal = NYSEExchangeCalendar() + elif name == 'CME': + cal = CMEExchangeCalendar() + elif name == 'BMF': + cal = BMFExchangeCalendar() + elif name == 'LSE': + cal = LSEExchangeCalendar() + elif name == 'TSX': + cal = TSXExchangeCalendar() + else: + raise InvalidCalendarName(calendar_name=name) + + register_calendar(cal) + + return _static_calendars[name] + + +def deregister_calendar(cal_name): + """ + If a calendar is registered with the given name, it is de-registered. + + Parameters + ---------- + cal_name : str + The name of the calendar to be deregistered. + """ + try: + _static_calendars.pop(cal_name) + except KeyError: + pass + + +def clear_calendars(): + """ + Deregisters all current registered calendars + """ + _static_calendars.clear() + + +def register_calendar(calendar, force=False): + """ + Registers a calendar for retrieval by the get_calendar method. + + Parameters + ---------- + calendar : TradingCalendar + The calendar to be registered for retrieval. + force : bool, optional + If True, old calendars will be overwritten on a name collision. + If False, name collisions will raise an exception. Default: False. + + Raises + ------ + CalendarNameCollision + If a calendar is already registered with the given calendar's name. + """ + # If we are forcing the registration, remove an existing calendar with the + # same name. + if force: + deregister_calendar(calendar.name) + + # Check if we are already holding a calendar with the same name + if calendar.name in _static_calendars: + raise CalendarNameCollision(calendar_name=calendar.name) + + _static_calendars[calendar.name] = calendar diff --git a/zipline/utils/calendars/exchange_calendar.py b/zipline/utils/calendars/exchange_calendar.py deleted file mode 100644 index 3f1cbb4f..00000000 --- a/zipline/utils/calendars/exchange_calendar.py +++ /dev/null @@ -1,588 +0,0 @@ -# -# Copyright 2016 Quantopian, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from abc import ( - ABCMeta, - abstractproperty, - abstractmethod, -) - -import pandas as pd -import numpy as np -from pandas import ( - DataFrame, - date_range, - DateOffset, - DatetimeIndex, -) -from pandas.tseries.offsets import CustomBusinessDay -from six import with_metaclass - -from zipline.errors import ( - InvalidCalendarName, - CalendarNameCollision, -) -from zipline.utils.memoize import remember_last - -from .calendar_helpers import ( - next_scheduled_day, - previous_scheduled_day, - next_open_and_close, - previous_open_and_close, - scheduled_day_distance, - minutes_for_day, - days_in_range, - minutes_for_days_in_range, - add_scheduled_days, - next_scheduled_minute, - previous_scheduled_minute, -) - -start_default = pd.Timestamp('1990-01-01', tz='UTC') -end_base = pd.Timestamp('today', tz='UTC') -# Give an aggressive buffer for logic that needs to use the next trading -# day or minute. -end_default = end_base + pd.Timedelta(days=365) - -NANOS_IN_MINUTE = 60000000000 - - -def days_at_time(days, t, tz, day_offset=0): - """ - Shift an index of days to time t, interpreted in tz. - - Overwrites any existing tz info on the input. - - Parameters - ---------- - days : DatetimeIndex - The "base" time which we want to change. - t : datetime.time - The time we want to offset @days by - tz : pytz.timezone - The timezone which these times represent - day_offset : int - The number of days we want to offset @days by - """ - days = DatetimeIndex(days).tz_localize(None).tz_localize(tz) - days_offset = days + DateOffset(day_offset) - return days_offset.shift( - 1, freq=DateOffset(hour=t.hour, minute=t.minute, second=t.second) - ).tz_convert('UTC') - - -def holidays_at_time(calendar, start, end, time, tz): - return days_at_time( - calendar.holidays( - # Workaround for https://github.com/pydata/pandas/issues/9825. - start.tz_localize(None), - end.tz_localize(None), - ), - time, - tz=tz, - ) - - -def _overwrite_special_dates(midnight_utcs, - opens_or_closes, - special_opens_or_closes): - """ - Overwrite dates in open_or_closes with corresponding dates in - special_opens_or_closes, using midnight_utcs for alignment. - """ - # Short circuit when nothing to apply. - if not len(special_opens_or_closes): - return - - len_m, len_oc = len(midnight_utcs), len(opens_or_closes) - if len_m != len_oc: - raise ValueError( - "Found misaligned dates while building calendar.\n" - "Expected midnight_utcs to be the same length as open_or_closes,\n" - "but len(midnight_utcs)=%d, len(open_or_closes)=%d" % len_m, len_oc - ) - - # Find the array indices corresponding to each special date. - indexer = midnight_utcs.get_indexer(special_opens_or_closes.normalize()) - - # -1 indicates that no corresponding entry was found. If any -1s are - # present, then we have special dates that doesn't correspond to any - # trading day. - if -1 in indexer: - bad_dates = list(special_opens_or_closes[indexer == -1]) - raise ValueError("Special dates %s are not trading days." % bad_dates) - - # NOTE: This is a slightly dirty hack. We're in-place overwriting the - # internal data of an Index, which is conceptually immutable. Since we're - # maintaining sorting, this should be ok, but this is a good place to - # sanity check if things start going haywire with calendar computations. - opens_or_closes.values[indexer] = special_opens_or_closes.values - - -class ExchangeCalendar(with_metaclass(ABCMeta)): - """ - An ExchangeCalendar represents the timing information of a single market - exchange. - - Properties - ---------- - name : str - The name of this exchange calendar. - e.g.: 'NYSE', 'LSE', 'CME Energy' - tz : timezone - The native timezone of the exchange. - """ - - def __init__(self, start=start_default, end=end_default): - tz = self.tz - open_offset = self.open_offset - close_offset = self.close_offset - - # Define those days on which the exchange is usually open. - self.day = CustomBusinessDay( - holidays=self.holidays_adhoc, - calendar=self.holidays_calendar, - ) - - # Midnight in UTC for each trading day. - _all_days = date_range(start, end, freq=self.day, tz='UTC') - - # `DatetimeIndex`s of standard opens/closes for each day. - self._opens = days_at_time(_all_days, self.open_time, tz, open_offset) - self._closes = days_at_time( - _all_days, self.close_time, tz, close_offset - ) - - # `DatetimeIndex`s of nonstandard opens/closes - _special_opens = self._special_opens(start, end) - _special_closes = self._special_closes(start, end) - - # Overwrite the special opens and closes on top of the standard ones. - _overwrite_special_dates(_all_days, self._opens, _special_opens) - _overwrite_special_dates(_all_days, self._closes, _special_closes) - - # In pandas 0.16.1 _opens and _closes will lose their timezone - # information. This looks like it has been resolved in 0.17.1. - # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa - self.schedule = DataFrame( - index=_all_days, - columns=['market_open', 'market_close'], - data={ - 'market_open': self._opens, - 'market_close': self._closes, - }, - dtype='datetime64[ns]', - ) - - self.first_trading_day = _all_days[0] - self.last_trading_day = _all_days[-1] - self.early_closes = DatetimeIndex( - _special_closes.map(self.session_date) - ) - - def next_trading_day(self, date): - return next_scheduled_day( - date, - last_trading_day=self.last_trading_day, - is_scheduled_day_hook=self.is_open_on_day, - ) - - def previous_trading_day(self, date): - return previous_scheduled_day( - date, - first_trading_day=self.first_trading_day, - is_scheduled_day_hook=self.is_open_on_day, - ) - - def next_open_and_close(self, date): - return next_open_and_close( - date, - open_and_close_hook=self.open_and_close, - next_scheduled_day_hook=self.next_trading_day, - ) - - def previous_open_and_close(self, date): - return previous_open_and_close( - date, - open_and_close_hook=self.open_and_close, - previous_scheduled_day_hook=self.previous_trading_day, - ) - - def trading_day_distance(self, first_date, second_date): - return scheduled_day_distance( - first_date, second_date, - all_days=self.all_trading_days, - ) - - def trading_minutes_for_day(self, day): - return minutes_for_day( - day, - open_and_close_hook=self.open_and_close, - ) - - def trading_days_in_range(self, start, end): - return days_in_range( - start, end, - all_days=self.all_trading_days, - ) - - def trading_minutes_for_days_in_range(self, start, end): - return minutes_for_days_in_range( - start, end, - days_in_range_hook=self.trading_days_in_range, - minutes_for_day_hook=self.trading_minutes_for_day, - ) - - def add_trading_days(self, n, date): - """ - Adds n trading days to date. If this would fall outside of the - ExchangeCalendar, a NoFurtherDataError is raised. - - Parameters - ---------- - n : int - The number of days to add to date, this can be positive or - negative. - date : datetime - The date to add to. - - Returns - ------- - datetime - n trading days added to date. - """ - return add_scheduled_days( - n, date, - next_scheduled_day_hook=self.next_trading_day, - previous_scheduled_day_hook=self.previous_trading_day, - all_trading_days=self.all_trading_days, - ) - - def next_trading_minute(self, start): - return next_scheduled_minute( - start, - is_scheduled_day_hook=self.is_open_on_day, - open_and_close_hook=self.open_and_close, - next_open_and_close_hook=self.next_open_and_close, - ) - - def previous_trading_minute(self, start): - return previous_scheduled_minute( - start, - is_scheduled_day_hook=self.is_open_on_day, - open_and_close_hook=self.open_and_close, - previous_open_and_close_hook=self.previous_open_and_close, - ) - - def _special_dates(self, calendars, ad_hoc_dates, start_date, end_date): - """ - Union an iterable of pairs of the form - - (time, calendar) - - and an iterable of pairs of the form - - (time, [dates]) - - (This is shared logic for computing special opens and special closes.) - """ - tz = self.native_timezone - _dates = DatetimeIndex([], tz='UTC').union_many( - [ - holidays_at_time(calendar, start_date, end_date, time_, tz) - for time_, calendar in calendars - ] + [ - days_at_time(datetimes, time_, tz) - for time_, datetimes in ad_hoc_dates - ] - ) - return _dates[(_dates >= start_date) & (_dates <= end_date)] - - def _special_opens(self, start, end): - return self._special_dates( - self.special_opens_calendars, - self.special_opens_adhoc, - start, - end, - ) - - def _special_closes(self, start, end): - return self._special_dates( - self.special_closes_calendars, - self.special_closes_adhoc, - start, - end, - ) - - @abstractproperty - def name(self): - """ - The name of this exchange calendar. - E.g.: 'NYSE', 'LSE', 'CME Energy' - """ - raise NotImplementedError() - - @abstractproperty - def tz(self): - """ - The native timezone of the exchange. - - SD: Not clear that this needs to be exposed. - """ - raise NotImplementedError() - - @abstractmethod - def is_open_on_minute(self, dt): - """ - Is the exchange open at minute @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at the given dt, otherwise False. - """ - raise NotImplementedError() - - @abstractmethod - def is_open_on_day(self, dt): - """ - Is the exchange open anytime during @dt. - - SD: Need to decide whether this method answers the question: - - Is exchange open at any time during the calendar day containing dt - or - - Is exchange open at any time during the trading session containg dt. - Semantically it seems that the first makes more sense. - - Parameters - ---------- - dt : Timestamp - The UTC-canonicalized date. - - Returns - ------- - bool - True if exchange is open at any time during @dt. - """ - raise NotImplementedError() - - @abstractmethod - def trading_days(self, start, end): - """ - Calculates all of the exchange sessions between the given - start and end. - - SD: Presumably @start and @end are UTC-canonicalized, as our exchange - sessions are. If not, then it's not clear how this method should behave - if @start and @end are both in the middle of the day. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the trading days between - the given start and end. - """ - raise NotImplementedError() - - @property - def all_trading_days(self): - return self.schedule.index - - @property - @remember_last - def all_trading_minutes(self): - opens_in_ns = \ - self._opens.values.astype('datetime64[ns]').astype(np.int64) - - closes_in_ns = \ - self._closes.values.astype('datetime64[ns]').astype(np.int64) - - deltas = closes_in_ns - opens_in_ns - - # + 1 because we want 390 days per standard day, not 389 - daily_sizes = (deltas / NANOS_IN_MINUTE) + 1 - num_minutes = np.sum(daily_sizes).astype(np.int64) - - # One allocation for the entire thing. This assumes that each day - # represents a contiguous block of minutes, which might not always - # be the case in the future. - all_minutes = np.empty(num_minutes, dtype='datetime64[ns]') - - idx = 0 - for day_idx, size in enumerate(daily_sizes): - # lots of small allocations, but it's fast enough for now. - all_minutes[idx:(idx + size)] = \ - np.arange( - opens_in_ns[day_idx], - closes_in_ns[day_idx] + NANOS_IN_MINUTE, - NANOS_IN_MINUTE - ) - - idx += size - - return DatetimeIndex(all_minutes).tz_localize("UTC") - - @abstractmethod - def open_and_close(self, date): - """ - Given a UTC-canonicalized date, returns a tuple of timestamps of the - open and close of the exchange session on that date. - - SD: Can @date be an arbitrary datetime, or should we first map it to - and exchange session using session_date. Need to check what the - consumers expect. - - Parameters - ---------- - date : Timestamp - The UTC-canonicalized date whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) - The open and close for the given date. - """ - raise NotImplementedError() - - @abstractmethod - def session_date(self, dt): - """ - Given a time, returns the UTC-canonicalized date of the exchange - session in which the time belongs. If the time is not in an exchange - session (while the market is closed), returns the date of the next - exchange session after the time. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - raise NotImplementedError() - - -_static_calendars = {} - - -def get_calendar(name): - """ - Retrieves an instance of an ExchangeCalendar whose name is given. - - Parameters - ---------- - name : str - The name of the ExchangeCalendar to be retrieved. - """ - # First, check if the calendar is already registered - if name not in _static_calendars: - - # Check if it is a lazy calendar. If so, build and register it. - if name == 'NYSE': - from zipline.utils.calendars.exchange_calendar_nyse \ - import NYSEExchangeCalendar - nyse_cal = NYSEExchangeCalendar() - register_calendar(nyse_cal) - - elif name == 'CME': - from zipline.utils.calendars.exchange_calendar_cme \ - import CMEExchangeCalendar - cme_cal = CMEExchangeCalendar() - register_calendar(cme_cal) - - elif name == 'BMF': - from zipline.utils.calendars.exchange_calendar_bmf \ - import BMFExchangeCalendar - bmf_cal = BMFExchangeCalendar() - register_calendar(bmf_cal) - - elif name == 'LSE': - from zipline.utils.calendars.exchange_calendar_lse \ - import LSEExchangeCalendar - lse_cal = LSEExchangeCalendar() - register_calendar(lse_cal) - - elif name == 'TSX': - from zipline.utils.calendars.exchange_calendar_tsx \ - import TSXExchangeCalendar - tsx_cal = TSXExchangeCalendar() - register_calendar(tsx_cal) - - else: - # It's not a lazy calendar, so raise an exception - raise InvalidCalendarName(calendar_name=name) - - return _static_calendars[name] - - -def deregister_calendar(cal_name): - """ - If a calendar is registered with the given name, it is de-registered. - - Parameters - ---------- - cal_name : str - The name of the calendar to be deregistered. - """ - try: - _static_calendars.pop(cal_name) - except KeyError: - pass - - -def clear_calendars(): - """ - Deregisters all current registered calendars - """ - _static_calendars.clear() - - -def register_calendar(calendar, force=False): - """ - Registers a calendar for retrieval by the get_calendar method. - - Parameters - ---------- - calendar : ExchangeCalendar - The calendar to be registered for retrieval. - force : bool, optional - If True, old calendars will be overwritten on a name collision. - If False, name collisions will raise an exception. Default: False. - - Raises - ------ - CalendarNameCollision - If a calendar is already registered with the given calendar's name. - """ - # If we are forcing the registration, remove an existing calendar with the - # same name. - if force: - deregister_calendar(calendar.name) - - # Check if we are already holding a calendar with the same name - if calendar.name in _static_calendars: - raise CalendarNameCollision(calendar_name=calendar.name) - - _static_calendars[calendar.name] = calendar diff --git a/zipline/utils/calendars/exchange_calendar_bmf.py b/zipline/utils/calendars/exchange_calendar_bmf.py index 7f154eaf..8d5e1d43 100644 --- a/zipline/utils/calendars/exchange_calendar_bmf.py +++ b/zipline/utils/calendars/exchange_calendar_bmf.py @@ -1,5 +1,4 @@ from datetime import time -from pandas import Timedelta from pandas.tseries.holiday import( AbstractHolidayCalendar, Holiday, @@ -9,10 +8,10 @@ from pandas.tseries.holiday import( ) from pytz import timezone -from zipline.utils.calendars.exchange_calendar import ExchangeCalendar -from zipline.utils.calendars.calendar_helpers import normalize_date - -MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) +from .trading_calendar import ( + TradingCalendar, + FRIDAY +) # Universal Confraternization (new years day) ConfUniversal = Holiday( @@ -170,7 +169,7 @@ class BMFLateOpenCalendar(AbstractHolidayCalendar): ] -class BMFExchangeCalendar(ExchangeCalendar): +class BMFExchangeCalendar(TradingCalendar): """ Exchange calendar for BM&F BOVESPA @@ -197,8 +196,8 @@ class BMFExchangeCalendar(ExchangeCalendar): - New Year's Eve (December 31) """ - exchange_name = 'BMF' - native_timezone = timezone('America/Sao_Paulo') + name = "BMF" + tz = timezone('America/Sao_Paulo') open_time = time(10, 1) close_time = time(17) @@ -217,160 +216,3 @@ class BMFExchangeCalendar(ExchangeCalendar): special_opens_adhoc = () special_closes_adhoc = () - - @property - def name(self): - """ - The name of this exchange calendar. - E.g.: 'NYSE', 'LSE', 'CME Energy' - """ - return self.exchange_name - - @property - def tz(self): - """ - The native timezone of the exchange. - """ - return self.native_timezone - - def is_open_on_minute(self, dt): - """ - Is the exchange open (accepting orders) at @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at the given dt, otherwise False. - """ - # Retrieve the exchange session relevant for this datetime - session = self.session_date(dt) - # Retrieve the open and close for this exchange session - open, close = self.open_and_close(session) - # Is @dt within the trading hours for this exchange session - return open <= dt and dt <= close - - def is_open_on_day(self, dt): - """ - Is the exchange open (accepting orders) anytime during the calendar day - containing @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at any time during the day containing @dt - """ - dt_normalized = normalize_date(dt) - return dt_normalized in self.schedule.index - - def trading_days(self, start, end): - """ - Calculates all of the exchange sessions between the given - start and end, inclusive. - - SD: Should @start and @end are UTC-canonicalized, as our exchange - sessions are. If not, then it's not clear how this method should behave - if @start and @end are both in the middle of the day. Here, I assume we - need to map @start and @end to session. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the trading days between - the given start and end. - """ - start_session = self.session_date(start) - end_session = self.session_date(end) - # Increment end_session by one day, beucase .loc[s:e] return all values - # in the DataFrame up to but not including `e`. - # end_session += Timedelta(days=1) - return self.schedule.loc[start_session:end_session] - - def open_and_close(self, dt): - """ - Given a datetime, returns a tuple of timestamps of the - open and close of the exchange session containing the datetime. - - SD: Should we accept an arbitrary datetime, or should we first map it - to and exchange session using session_date. Need to check what the - consumers expect. Here, I assume we need to map it to a session. - - Parameters - ---------- - dt : Timestamp - A dt in a session whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) - The open and close for the given dt. - """ - session = self.session_date(dt) - return self._get_open_and_close(session) - - def _get_open_and_close(self, session_date): - """ - Retrieves the open and close for a given session. - - Parameters - ---------- - session_date : Timestamp - The canonicalized session_date whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) or (None, None) - The open and close for the given dt, or Nones if the given date is - not a session. - """ - # Return a tuple of nones if the given date is not a session. - if session_date not in self.schedule.index: - return (None, None) - - o_and_c = self.schedule.loc[session_date] - # `market_open` and `market_close` should be timezone aware, but pandas - # 0.16.1 does not appear to support this: - # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa - return (o_and_c['market_open'].tz_localize('UTC'), - o_and_c['market_close'].tz_localize('UTC')) - - def session_date(self, dt): - """ - Given a datetime, returns the UTC-canonicalized date of the exchange - session in which the time belongs. If the time is not in an exchange - session (while the market is closed), returns the date of the next - exchange session after the time. - - Parameters - ---------- - dt : Timestamp - A timezone-aware Timestamp. - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - # Check if the dt is after the market close - # If so, advance to the next day - if self.is_open_on_day(dt): - _, close = self._get_open_and_close(normalize_date(dt)) - if dt > close: - dt += Timedelta(days=1) - - while not self.is_open_on_day(dt): - dt += Timedelta(days=1) - - return normalize_date(dt) diff --git a/zipline/utils/calendars/exchange_calendar_cme.py b/zipline/utils/calendars/exchange_calendar_cme.py index 5ff01ce1..f571af62 100644 --- a/zipline/utils/calendars/exchange_calendar_cme.py +++ b/zipline/utils/calendars/exchange_calendar_cme.py @@ -16,154 +16,44 @@ from datetime import time from itertools import chain -from dateutil.relativedelta import ( - MO, - TH, -) -from pandas import ( - date_range, - DateOffset, - Timedelta, - Timestamp, -) -from pandas.tseries.holiday import( - AbstractHolidayCalendar, - GoodFriday, - Holiday, - nearest_workday, - sunday_to_monday, - USLaborDay, - USPresidentsDay, - USThanksgivingDay, -) -from pandas.tseries.offsets import Day +from pandas.tseries.holiday import AbstractHolidayCalendar from pytz import timezone -from zipline.utils.calendars import ExchangeCalendar -from .calendar_helpers import normalize_date - # Useful resources for making changes to this file: -# http://www.nyse.com/pdfs/closings.pdf -# http://www.stevemorse.org/jcal/whendid.html +# http://www.cmegroup.com/tools-information/holiday-calendar.html -MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) +from .trading_calendar import TradingCalendar + +from .us_holidays import ( + USNewYearsDay, + Christmas, + ChristmasEveBefore1993, + ChristmasEveInOrAfter1993, + FridayAfterIndependenceDayExcept2013, + MonTuesThursBeforeIndependenceDay, + USBlackFridayInOrAfter1993, + September11Closings, + USNationalDaysofMourning +) US_CENTRAL = timezone('America/Chicago') CME_OPEN = time(17) CME_CLOSE = time(16) -# CME_STANDARD_EARLY_CLOSE = time(13) + +# The CME seems to have different holiday rules depending on the type +# of instrument. For example, http://www.cmegroup.com/tools-information/holiday-calendar/files/2016-4th-of-july-holiday-schedule.pdf # noqa +# shows that Equity, Interest Rate, FX, Energy, Metals & DME Products close at +# 1200 CT on July 4, 2016, while Grain, Oilseed & MGEX Products and Livestock, +# Dairy & Lumber products are completely closed. + +# For now, we will treat the CME as having a single calendar, and just go with +# the most conservative hours - and treat July 4 as an early close at noon. +CME_STANDARD_EARLY_CLOSE = time(12) + # Does the market open or close on a different calendar day, compared to the -# calendar day assigned by the exchang to this session? +# calendar day assigned by the exchange to this session? CME_OPEN_OFFSET = -1 -CME_CLOSE_OFFSET = 0 - -# Closings -USNewYearsDay = Holiday( - 'New Years Day', - month=1, - day=1, - # When Jan 1 is a Sunday, NYSE observes the subsequent Monday. When Jan 1 - # Saturday (as in 2005 and 2011), no holiday is observed. - observance=sunday_to_monday -) -USMemorialDay = Holiday( - # NOTE: The definition for Memorial Day is incorrect as of pandas 0.16.0. - # See https://github.com/pydata/pandas/issues/9760. - 'Memorial Day', - month=5, - day=25, - offset=DateOffset(weekday=MO(1)), -) -USMartinLutherKingJrAfter1998 = Holiday( - 'Dr. Martin Luther King Jr. Day', - month=1, - day=1, - # The NYSE didn't observe MLK day as a holiday until 1998. - start_date=Timestamp('1998-01-01'), - offset=DateOffset(weekday=MO(3)), -) -USIndependenceDay = Holiday( - 'July 4th', - month=7, - day=4, - observance=nearest_workday, -) -Christmas = Holiday( - 'Christmas', - month=12, - day=25, - observance=nearest_workday, -) - -# Half Days -MonTuesThursBeforeIndependenceDay = Holiday( - # When July 4th is a Tuesday, Wednesday, or Friday, the previous day is a - # half day. - 'Mondays, Tuesdays, and Thursdays Before Independence Day', - month=7, - day=3, - days_of_week=(MONDAY, TUESDAY, THURSDAY), - start_date=Timestamp("1995-01-01"), -) -FridayAfterIndependenceDayExcept2013 = Holiday( - # When July 4th is a Thursday, the next day is a half day (except in 2013, - # when, for no explicable reason, Wednesday was a half day instead). - "Fridays after Independence Day that aren't in 2013", - month=7, - day=5, - days_of_week=(FRIDAY,), - observance=lambda dt: None if dt.year == 2013 else dt, - start_date=Timestamp("1995-01-01"), -) -USBlackFridayBefore1993 = Holiday( - 'Black Friday', - month=11, - day=1, - # Black Friday was not observed until 1992. - start_date=Timestamp('1992-01-01'), - end_date=Timestamp('1993-01-01'), - offset=[DateOffset(weekday=TH(4)), Day(1)], -) -USBlackFridayInOrAfter1993 = Holiday( - 'Black Friday', - month=11, - day=1, - start_date=Timestamp('1993-01-01'), - offset=[DateOffset(weekday=TH(4)), Day(1)], -) -# These have the same definition, but are used in different places because the -# NYSE closed at 2:00 PM on Christmas Eve until 1993. -ChristmasEveBefore1993 = Holiday( - 'Christmas Eve', - month=12, - day=24, - end_date=Timestamp('1993-01-01'), - # When Christmas is a Saturday, the 24th is a full holiday. - days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY), -) -ChristmasEveInOrAfter1993 = Holiday( - 'Christmas Eve', - month=12, - day=24, - start_date=Timestamp('1993-01-01'), - # When Christmas is a Saturday, the 24th is a full holiday. - days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY), -) - - -# http://en.wikipedia.org/wiki/Aftermath_of_the_September_11_attacks -September11Closings = date_range('2001-09-11', '2001-09-16', tz='UTC') - - -# National Days of Mourning -# - President Richard Nixon - April 27, 1994 -# - President Ronald W. Reagan - June 11, 2004 -# - President Gerald R. Ford - Jan 2, 2007 -USNationalDaysofMourning = [ - Timestamp('1994-04-27', tz='UTC'), - Timestamp('2004-06-11', tz='UTC'), - Timestamp('2007-01-02', tz='UTC'), -] +CME_CLOSE_OFFSET = -0 class CMEHolidayCalendar(AbstractHolidayCalendar): @@ -174,14 +64,6 @@ class CMEHolidayCalendar(AbstractHolidayCalendar): """ rules = [ USNewYearsDay, - USMartinLutherKingJrAfter1998, - USPresidentsDay, - GoodFriday, - USMemorialDay, - USIndependenceDay, - USLaborDay, - USThanksgivingDay, - USIndependenceDay, Christmas, ] @@ -194,15 +76,16 @@ class CMEEarlyCloseCalendar(AbstractHolidayCalendar): MonTuesThursBeforeIndependenceDay, FridayAfterIndependenceDayExcept2013, USBlackFridayInOrAfter1993, + ChristmasEveBefore1993, ChristmasEveInOrAfter1993, ] -class CMEExchangeCalendar(ExchangeCalendar): +class CMEExchangeCalendar(TradingCalendar): """ Exchange calendar for CME - Open Time: 5:00 AM, America/Chicago + Open Time: 5:00 PM, America/Chicago Close Time: 5:00 PM, America/Chicago Regularly-Observed Holidays: @@ -216,19 +99,16 @@ class CMEExchangeCalendar(ExchangeCalendar): - Thanksgiving (fourth Thursday in November) - Christmas (observed on nearest weekday to December 25) - NOTE: The CME does not observe the following US Federal Holidays: + NOTE: For the following US Federal Holidays, part of the CME is closed + (Foreign Exchange, Interest Rates) but Commodities, GSCI, Weather & Real + Estate is open. Thus, we don't treat these as holidays. - Columbus Day - Veterans Day Regularly-Observed Early Closes: - - July 3rd (Mondays, Tuesdays, and Thursdays, 1995 onward) - - July 5th (Fridays, 1995 onward, except 2013) - Christmas Eve (except on Fridays, when the exchange is closed entirely) - Day After Thanksgiving (aka Black Friday, observed from 1992 onward) - NOTE: Until 1993, the standard early close time for the NYSE was 2:00 PM. - From 1993 onward, it has been 1:00 PM. - Additional Irregularities: - Closed from 9/11/2001 to 9/16/2001 due to terrorist attacks in NYC. - Closed on 10/29/2012 and 10/30/2012 due to Hurricane Sandy. @@ -246,7 +126,8 @@ class CMEExchangeCalendar(ExchangeCalendar): we've done alright...and we should check if it's a half day. """ - native_timezone = US_CENTRAL + name = "CME" + tz = US_CENTRAL open_time = CME_OPEN close_time = CME_CLOSE open_offset = CME_OPEN_OFFSET @@ -263,157 +144,3 @@ class CMEExchangeCalendar(ExchangeCalendar): special_opens_adhoc = () special_closes_adhoc = [] - - @property - def name(self): - """ - The name of this exchange calendar. - E.g.: 'NYSE', 'LSE', 'CME Energy' - """ - return 'CME' - - @property - def tz(self): - """ - The native timezone of the exchange. - - SD: Not clear that this needs to be exposed. - """ - return self.native_timezone - - def is_open_on_minute(self, dt): - """ - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at the given dt, otherwise False. - """ - # Retrieve the exchange session relevant for this datetime - session = self.session_date(dt) - # Retrieve the opens and closes for this exchange session - session_open, session_close = self.open_and_close(session) - # Is @dt within the trading hours for this exchange session - return ( - session_open and session_close and - session_open <= dt <= session_close - ) - - def is_open_on_day(self, dt): - """ - Is the exchange open (accepting orders) anytime during the calendar day - containing @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at any time during the day containing @dt - """ - dt_normalized = normalize_date(dt) - return dt_normalized in self.schedule.index - - def trading_days(self, start, end): - """ - Calculates all of the exchange sessions between the given - start and end. - - SD: Presumably @start and @end are UTC-canonicalized, as our exchange - sessions are. If not, then it's not clear how this method should behave - if @start and @end are both in the middle of the day. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the trading days between - the given start and end. - """ - return self.schedule.index[start:end] - - def open_and_close(self, dt): - """ - Given a UTC-canonicalized date, returns a tuple of timestamps of the - open and close of the exchange session on that date. - - SD: Can @date be an arbitrary datetime, or should we first map it to - and exchange session using session_date. Need to check what the - consumers expect. Here, I assume we need to map it to a session. - - Parameters - ---------- - session : Timestamp - The UTC-canonicalized session whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) - The open and close for the given date. - """ - session = self.session_date(dt) - return self._get_open_and_close(session) - - def _get_open_and_close(self, session_date): - """ - Retrieves the open and close for a given session. - - Parameters - ---------- - session_date : Timestamp - The canonicalized session_date whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) or (None, None) - The open and close for the given dt, or Nones if the given date is - not a session. - """ - # Return a tuple of nones if the given date is not a session. - if session_date not in self.schedule.index: - return (None, None) - - o_and_c = self.schedule.loc[session_date] - # `market_open` and `market_close` should be timezone aware, but pandas - # 0.16.1 does not appear to support this: - # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa - return (o_and_c['market_open'].tz_localize('UTC'), - o_and_c['market_close'].tz_localize('UTC')) - - def session_date(self, dt): - """ - Given a time, returns the UTC-canonicalized date of the exchange - session in which the time belongs. If the time is not in an exchange - session (while the market is closed), returns the date of the next - exchange session after the time. - - Parameters - ---------- - dt : Timestamp - A timezone-aware Timestamp. - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - # Check if the dt is after the market close - # If so, advance to the next day - if self.is_open_on_day(dt): - _, close = self._get_open_and_close(normalize_date(dt)) - if dt > close: - dt += Timedelta(days=1) - - while not self.is_open_on_day(dt): - dt += Timedelta(days=1) - - return normalize_date(dt) diff --git a/zipline/utils/calendars/exchange_calendar_lse.py b/zipline/utils/calendars/exchange_calendar_lse.py index 3e2be253..6b959ff1 100644 --- a/zipline/utils/calendars/exchange_calendar_lse.py +++ b/zipline/utils/calendars/exchange_calendar_lse.py @@ -1,5 +1,4 @@ from datetime import time -from pandas import Timedelta from pandas.tseries.holiday import( AbstractHolidayCalendar, Holiday, @@ -11,10 +10,11 @@ from pandas.tseries.holiday import( ) from pytz import timezone -from zipline.utils.calendars.exchange_calendar import ExchangeCalendar -from zipline.utils.calendars.calendar_helpers import normalize_date - -MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) +from .trading_calendar import ( + TradingCalendar, + MONDAY, + TUESDAY, +) # New Year's Day LSENewYearsDay = Holiday( @@ -93,7 +93,7 @@ class LSEHolidayCalendar(AbstractHolidayCalendar): ] -class LSEExchangeCalendar(ExchangeCalendar): +class LSEExchangeCalendar(TradingCalendar): """ Exchange calendar for the London Stock Exchange @@ -113,8 +113,8 @@ class LSEExchangeCalendar(ExchangeCalendar): - Dec. 28th (if Boxing Day is on a weekend) """ - exchange_name = 'LSE' - native_timezone = timezone('Europe/London') + name = 'LSE' + tz = timezone('Europe/London') open_time = time(8, 1) close_time = time(16, 30) open_offset = 0 @@ -128,160 +128,3 @@ class LSEExchangeCalendar(ExchangeCalendar): special_opens_adhoc = () special_closes_adhoc = () - - @property - def name(self): - """ - The name of this exchange calendar. - E.g.: 'NYSE', 'LSE', 'CME Energy' - """ - return self.exchange_name - - @property - def tz(self): - """ - The native timezone of the exchange. - """ - return self.native_timezone - - def is_open_on_minute(self, dt): - """ - Is the exchange open (accepting orders) at @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at the given dt, otherwise False. - """ - # Retrieve the exchange session relevant for this datetime - session = self.session_date(dt) - # Retrieve the open and close for this exchange session - open, close = self.open_and_close(session) - # Is @dt within the trading hours for this exchange session - return open <= dt and dt <= close - - def is_open_on_day(self, dt): - """ - Is the exchange open (accepting orders) anytime during the calendar day - containing @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at any time during the day containing @dt - """ - dt_normalized = normalize_date(dt) - return dt_normalized in self.schedule.index - - def trading_days(self, start, end): - """ - Calculates all of the exchange sessions between the given - start and end, inclusive. - - SD: Should @start and @end are UTC-canonicalized, as our exchange - sessions are. If not, then it's not clear how this method should behave - if @start and @end are both in the middle of the day. Here, I assume we - need to map @start and @end to session. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the trading days between - the given start and end. - """ - start_session = self.session_date(start) - end_session = self.session_date(end) - # Increment end_session by one day, beucase .loc[s:e] return all values - # in the DataFrame up to but not including `e`. - # end_session += Timedelta(days=1) - return self.schedule.loc[start_session:end_session] - - def open_and_close(self, dt): - """ - Given a datetime, returns a tuple of timestamps of the - open and close of the exchange session containing the datetime. - - SD: Should we accept an arbitrary datetime, or should we first map it - to and exchange session using session_date. Need to check what the - consumers expect. Here, I assume we need to map it to a session. - - Parameters - ---------- - dt : Timestamp - A dt in a session whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) - The open and close for the given dt. - """ - session = self.session_date(dt) - return self._get_open_and_close(session) - - def _get_open_and_close(self, session_date): - """ - Retrieves the open and close for a given session. - - Parameters - ---------- - session_date : Timestamp - The canonicalized session_date whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) or (None, None) - The open and close for the given dt, or Nones if the given date is - not a session. - """ - # Return a tuple of nones if the given date is not a session. - if session_date not in self.schedule.index: - return (None, None) - - o_and_c = self.schedule.loc[session_date] - # `market_open` and `market_close` should be timezone aware, but pandas - # 0.16.1 does not appear to support this: - # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa - return (o_and_c['market_open'].tz_localize('UTC'), - o_and_c['market_close'].tz_localize('UTC')) - - def session_date(self, dt): - """ - Given a datetime, returns the UTC-canonicalized date of the exchange - session in which the time belongs. If the time is not in an exchange - session (while the market is closed), returns the date of the next - exchange session after the time. - - Parameters - ---------- - dt : Timestamp - A timezone-aware Timestamp. - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - # Check if the dt is after the market close - # If so, advance to the next day - if self.is_open_on_day(dt): - _, close = self._get_open_and_close(normalize_date(dt)) - if dt > close: - dt += Timedelta(days=1) - - while not self.is_open_on_day(dt): - dt += Timedelta(days=1) - - return normalize_date(dt) diff --git a/zipline/utils/calendars/exchange_calendar_nyse.py b/zipline/utils/calendars/exchange_calendar_nyse.py index c3913d92..af42cdc1 100644 --- a/zipline/utils/calendars/exchange_calendar_nyse.py +++ b/zipline/utils/calendars/exchange_calendar_nyse.py @@ -16,163 +16,48 @@ from datetime import time from itertools import chain -from dateutil.relativedelta import ( - MO, - TH, -) -from pandas import ( - date_range, - DateOffset, - Timestamp, - Timedelta, -) from pandas.tseries.holiday import( AbstractHolidayCalendar, GoodFriday, - Holiday, - nearest_workday, - sunday_to_monday, USLaborDay, USPresidentsDay, USThanksgivingDay, ) -from pandas.tseries.offsets import Day from pytz import timezone -from zipline.utils.pandas_utils import july_5th_holiday_observance -from .exchange_calendar import ExchangeCalendar -from .calendar_helpers import normalize_date +from .trading_calendar import TradingCalendar + +from .us_holidays import ( + USNewYearsDay, + USMartinLutherKingJrAfter1998, + USMemorialDay, + USIndependenceDay, + Christmas, + MonTuesThursBeforeIndependenceDay, + FridayAfterIndependenceDayExcept2013, + USBlackFridayBefore1993, + USBlackFridayInOrAfter1993, + September11Closings, + HurricaneSandyClosings, + USNationalDaysofMourning, + ChristmasEveBefore1993, + ChristmasEveInOrAfter1993, +) # Useful resources for making changes to this file: # http://www.nyse.com/pdfs/closings.pdf # http://www.stevemorse.org/jcal/whendid.html -MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) - US_EASTERN = timezone('US/Eastern') NYSE_OPEN = time(9, 31) NYSE_CLOSE = time(16) NYSE_STANDARD_EARLY_CLOSE = time(13) -# Does the market open or close on a different calendar day, compared to the -# calendar day assigned by the exchange to this session? + +# Whether market opens or closes on a different calendar day, compared to the +# calendar day assigned by the exchange to this session. NYSE_OPEN_OFFSET = 0 NYSE_CLOSE_OFFSET = 0 -# Closings -USNewYearsDay = Holiday( - 'New Years Day', - month=1, - day=1, - # When Jan 1 is a Sunday, NYSE observes the subsequent Monday. When Jan 1 - # Saturday (as in 2005 and 2011), no holiday is observed. - observance=sunday_to_monday -) -USMemorialDay = Holiday( - # NOTE: The definition for Memorial Day is incorrect as of pandas 0.16.0. - # See https://github.com/pydata/pandas/issues/9760. - 'Memorial Day', - month=5, - day=25, - offset=DateOffset(weekday=MO(1)), -) -USMartinLutherKingJrAfter1998 = Holiday( - 'Dr. Martin Luther King Jr. Day', - month=1, - day=1, - # The NYSE didn't observe MLK day as a holiday until 1998. - start_date=Timestamp('1998-01-01'), - offset=DateOffset(weekday=MO(3)), -) -USIndependenceDay = Holiday( - 'July 4th', - month=7, - day=4, - observance=nearest_workday, -) -Christmas = Holiday( - 'Christmas', - month=12, - day=25, - observance=nearest_workday, -) - -# Half Days -MonTuesThursBeforeIndependenceDay = Holiday( - # When July 4th is a Tuesday, Wednesday, or Friday, the previous day is a - # half day. - 'Mondays, Tuesdays, and Thursdays Before Independence Day', - month=7, - day=3, - days_of_week=(MONDAY, TUESDAY, THURSDAY), - start_date=Timestamp("1995-01-01"), -) -FridayAfterIndependenceDayExcept2013 = Holiday( - # When July 4th is a Thursday, the next day is a half day (except in 2013, - # when, for no explicable reason, Wednesday was a half day instead). - "Fridays after Independence Day that aren't in 2013", - month=7, - day=5, - days_of_week=(FRIDAY,), - # The 2013 observance lambda is pandas version-dependent - observance=july_5th_holiday_observance, - start_date=Timestamp("1995-01-01"), -) -USBlackFridayBefore1993 = Holiday( - 'Black Friday', - month=11, - day=1, - # Black Friday was not observed until 1992. - start_date=Timestamp('1992-01-01'), - end_date=Timestamp('1993-01-01'), - offset=[DateOffset(weekday=TH(4)), Day(1)], -) -USBlackFridayInOrAfter1993 = Holiday( - 'Black Friday', - month=11, - day=1, - start_date=Timestamp('1993-01-01'), - offset=[DateOffset(weekday=TH(4)), Day(1)], -) -# These have the same definition, but are used in different places because the -# NYSE closed at 2:00 PM on Christmas Eve until 1993. -ChristmasEveBefore1993 = Holiday( - 'Christmas Eve', - month=12, - day=24, - end_date=Timestamp('1993-01-01'), - # When Christmas is a Saturday, the 24th is a full holiday. - days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY), -) -ChristmasEveInOrAfter1993 = Holiday( - 'Christmas Eve', - month=12, - day=24, - start_date=Timestamp('1993-01-01'), - # When Christmas is a Saturday, the 24th is a full holiday. - days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY), -) - - -# http://en.wikipedia.org/wiki/Aftermath_of_the_September_11_attacks -September11Closings = date_range('2001-09-11', '2001-09-16', tz='UTC') - -# http://en.wikipedia.org/wiki/Hurricane_sandy -HurricaneSandyClosings = date_range( - '2012-10-29', - '2012-10-30', - tz='UTC' -) - -# National Days of Mourning -# - President Richard Nixon - April 27, 1994 -# - President Ronald W. Reagan - June 11, 2004 -# - President Gerald R. Ford - Jan 2, 2007 -USNationalDaysofMourning = [ - Timestamp('1994-04-27', tz='UTC'), - Timestamp('2004-06-11', tz='UTC'), - Timestamp('2007-01-02', tz='UTC'), -] - class NYSEHolidayCalendar(AbstractHolidayCalendar): """ @@ -216,7 +101,7 @@ class NYSEEarlyCloseCalendar(AbstractHolidayCalendar): ] -class NYSEExchangeCalendar(ExchangeCalendar): +class NYSEExchangeCalendar(TradingCalendar): """ Exchange calendar for NYSE @@ -264,8 +149,8 @@ class NYSEExchangeCalendar(ExchangeCalendar): we've done alright...and we should check if it's a half day. """ - exchange_name = 'NYSE' - native_timezone = US_EASTERN + name = "NYSE" + tz = US_EASTERN open_time = NYSE_OPEN close_time = NYSE_CLOSE open_offset = NYSE_OPEN_OFFSET @@ -291,160 +176,3 @@ class NYSEExchangeCalendar(ExchangeCalendar): '2003-12-26', '2013-07-03')), ] - - @property - def name(self): - """ - The name of this exchange calendar. - E.g.: 'NYSE', 'LSE', 'CME Energy' - """ - return self.exchange_name - - @property - def tz(self): - """ - The native timezone of the exchange. - """ - return self.native_timezone - - def is_open_on_minute(self, dt): - """ - Is the exchange open (accepting orders) at @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at the given dt, otherwise False. - """ - # Retrieve the exchange session relevant for this datetime - session = self.session_date(dt) - # Retrieve the open and close for this exchange session - open, close = self.open_and_close(session) - # Is @dt within the trading hours for this exchange session - return open <= dt and dt <= close - - def is_open_on_day(self, dt): - """ - Is the exchange open (accepting orders) anytime during the calendar day - containing @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at any time during the day containing @dt - """ - dt_normalized = normalize_date(dt) - return dt_normalized in self.schedule.index - - def trading_days(self, start, end): - """ - Calculates all of the exchange sessions between the given - start and end, inclusive. - - SD: Should @start and @end are UTC-canonicalized, as our exchange - sessions are. If not, then it's not clear how this method should behave - if @start and @end are both in the middle of the day. Here, I assume we - need to map @start and @end to session. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the trading days between - the given start and end. - """ - start_session = self.session_date(start) - end_session = self.session_date(end) - # Increment end_session by one day, beucase .loc[s:e] return all values - # in the DataFrame up to but not including `e`. - # end_session += Timedelta(days=1) - return self.schedule.loc[start_session:end_session] - - def open_and_close(self, dt): - """ - Given a datetime, returns a tuple of timestamps of the - open and close of the exchange session containing the datetime. - - SD: Should we accept an arbitrary datetime, or should we first map it - to and exchange session using session_date. Need to check what the - consumers expect. Here, I assume we need to map it to a session. - - Parameters - ---------- - dt : Timestamp - A dt in a session whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) - The open and close for the given dt. - """ - session = self.session_date(dt) - return self._get_open_and_close(session) - - def _get_open_and_close(self, session_date): - """ - Retrieves the open and close for a given session. - - Parameters - ---------- - session_date : Timestamp - The canonicalized session_date whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) or (None, None) - The open and close for the given dt, or Nones if the given date is - not a session. - """ - # Return a tuple of nones if the given date is not a session. - if session_date not in self.schedule.index: - return (None, None) - - o_and_c = self.schedule.loc[session_date] - # `market_open` and `market_close` should be timezone aware, but pandas - # 0.16.1 does not appear to support this: - # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa - return (o_and_c['market_open'].tz_localize('UTC'), - o_and_c['market_close'].tz_localize('UTC')) - - def session_date(self, dt): - """ - Given a datetime, returns the UTC-canonicalized date of the exchange - session in which the time belongs. If the time is not in an exchange - session (while the market is closed), returns the date of the next - exchange session after the time. - - Parameters - ---------- - dt : Timestamp - A timezone-aware Timestamp. - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - # Check if the dt is after the market close - # If so, advance to the next day - if self.is_open_on_day(dt): - _, close = self._get_open_and_close(normalize_date(dt)) - if dt > close: - dt += Timedelta(days=1) - - while not self.is_open_on_day(dt): - dt += Timedelta(days=1) - - return normalize_date(dt) diff --git a/zipline/utils/calendars/exchange_calendar_tsx.py b/zipline/utils/calendars/exchange_calendar_tsx.py index 5fb571f8..e51e27ed 100644 --- a/zipline/utils/calendars/exchange_calendar_tsx.py +++ b/zipline/utils/calendars/exchange_calendar_tsx.py @@ -1,5 +1,4 @@ from datetime import time -from pandas import Timedelta from pandas.tseries.holiday import( AbstractHolidayCalendar, Holiday, @@ -10,17 +9,14 @@ from pandas.tseries.holiday import( ) from pytz import timezone -from zipline.utils.calendars.exchange_calendar import ExchangeCalendar -from zipline.utils.calendars.calendar_helpers import normalize_date +from zipline.utils.calendars.trading_calendar import TradingCalendar +from zipline.utils.calendars.us_holidays import Christmas from zipline.utils.calendars.exchange_calendar_lse import ( - Christmas, WeekendChristmas, BoxingDay, WeekendBoxingDay, ) -MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) - # New Year's Day TSXNewYearsDay = Holiday( "New Year's Day", @@ -95,7 +91,7 @@ class TSXHolidayCalendar(AbstractHolidayCalendar): ] -class TSXExchangeCalendar(ExchangeCalendar): +class TSXExchangeCalendar(TradingCalendar): """ Exchange calendar for the Toronto Stock Exchange @@ -117,8 +113,8 @@ class TSXExchangeCalendar(ExchangeCalendar): - Dec. 28th (if Boxing Day is on a weekend) """ - exchange_name = 'TSX' - native_timezone = timezone('Canada/Atlantic') + name = 'TSX' + tz = timezone('Canada/Atlantic') open_time = time(9, 31) close_time = time(16) open_offset = 0 @@ -132,160 +128,3 @@ class TSXExchangeCalendar(ExchangeCalendar): special_opens_adhoc = () special_closes_adhoc = () - - @property - def name(self): - """ - The name of this exchange calendar. - E.g.: 'NYSE', 'LSE', 'CME Energy' - """ - return self.exchange_name - - @property - def tz(self): - """ - The native timezone of the exchange. - """ - return self.native_timezone - - def is_open_on_minute(self, dt): - """ - Is the exchange open (accepting orders) at @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at the given dt, otherwise False. - """ - # Retrieve the exchange session relevant for this datetime - session = self.session_date(dt) - # Retrieve the open and close for this exchange session - open, close = self.open_and_close(session) - # Is @dt within the trading hours for this exchange session - return open <= dt and dt <= close - - def is_open_on_day(self, dt): - """ - Is the exchange open (accepting orders) anytime during the calendar day - containing @dt. - - Parameters - ---------- - dt : Timestamp - - Returns - ------- - bool - True if exchange is open at any time during the day containing @dt - """ - dt_normalized = normalize_date(dt) - return dt_normalized in self.schedule.index - - def trading_days(self, start, end): - """ - Calculates all of the exchange sessions between the given - start and end, inclusive. - - SD: Should @start and @end are UTC-canonicalized, as our exchange - sessions are. If not, then it's not clear how this method should behave - if @start and @end are both in the middle of the day. Here, I assume we - need to map @start and @end to session. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex populated with all of the trading days between - the given start and end. - """ - start_session = self.session_date(start) - end_session = self.session_date(end) - # Increment end_session by one day, beucase .loc[s:e] return all values - # in the DataFrame up to but not including `e`. - # end_session += Timedelta(days=1) - return self.schedule.loc[start_session:end_session] - - def open_and_close(self, dt): - """ - Given a datetime, returns a tuple of timestamps of the - open and close of the exchange session containing the datetime. - - SD: Should we accept an arbitrary datetime, or should we first map it - to and exchange session using session_date. Need to check what the - consumers expect. Here, I assume we need to map it to a session. - - Parameters - ---------- - dt : Timestamp - A dt in a session whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) - The open and close for the given dt. - """ - session = self.session_date(dt) - return self._get_open_and_close(session) - - def _get_open_and_close(self, session_date): - """ - Retrieves the open and close for a given session. - - Parameters - ---------- - session_date : Timestamp - The canonicalized session_date whose open and close are needed. - - Returns - ------- - (Timestamp, Timestamp) or (None, None) - The open and close for the given dt, or Nones if the given date is - not a session. - """ - # Return a tuple of nones if the given date is not a session. - if session_date not in self.schedule.index: - return (None, None) - - o_and_c = self.schedule.loc[session_date] - # `market_open` and `market_close` should be timezone aware, but pandas - # 0.16.1 does not appear to support this: - # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa - return (o_and_c['market_open'].tz_localize('UTC'), - o_and_c['market_close'].tz_localize('UTC')) - - def session_date(self, dt): - """ - Given a datetime, returns the UTC-canonicalized date of the exchange - session in which the time belongs. If the time is not in an exchange - session (while the market is closed), returns the date of the next - exchange session after the time. - - Parameters - ---------- - dt : Timestamp - A timezone-aware Timestamp. - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - # Check if the dt is after the market close - # If so, advance to the next day - if self.is_open_on_day(dt): - _, close = self._get_open_and_close(normalize_date(dt)) - if dt > close: - dt += Timedelta(days=1) - - while not self.is_open_on_day(dt): - dt += Timedelta(days=1) - - return normalize_date(dt) diff --git a/zipline/utils/calendars/trading_calendar.py b/zipline/utils/calendars/trading_calendar.py new file mode 100644 index 00000000..2d480058 --- /dev/null +++ b/zipline/utils/calendars/trading_calendar.py @@ -0,0 +1,732 @@ +# +# Copyright 2016 Quantopian, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from abc import ABCMeta +from six import with_metaclass +from numpy import searchsorted +import numpy as np +import pandas as pd +from pandas import ( + DataFrame, + date_range, + DatetimeIndex, + DateOffset +) +from pandas.tseries.offsets import CustomBusinessDay + +from zipline.utils.calendars._calendar_helpers import ( + next_divider_idx, + previous_divider_idx, + is_open +) +from zipline.utils.memoize import remember_last + +start_default = pd.Timestamp('1990-01-01', tz='UTC') +end_base = pd.Timestamp('today', tz='UTC') +# Give an aggressive buffer for logic that needs to use the next trading +# day or minute. +end_default = end_base + pd.Timedelta(days=365) + +NANOS_IN_MINUTE = 60000000000 +MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) + + +class TradingCalendar(with_metaclass(ABCMeta)): + """ + An TradingCalendar represents the timing information of a single market + exchange. + + The timing information is made up of two parts: sessions, and opens/closes. + + A session represents a contiguous set of minutes, and has a label that is + midnight UTC. It is important to note that a session label should not be + considered a specific point in time, and that midnight UTC is just being + used for convenience. + + For each session, we store the open and close time in UTC time. + """ + def __init__(self, start=start_default, end=end_default): + open_offset = self.open_offset + close_offset = self.close_offset + + # Define those days on which the exchange is usually open. + self.day = CustomBusinessDay( + holidays=self.holidays_adhoc, + calendar=self.holidays_calendar, + ) + + # Midnight in UTC for each trading day. + _all_days = date_range(start, end, freq=self.day, tz='UTC') + + # `DatetimeIndex`s of standard opens/closes for each day. + self._opens = days_at_time(_all_days, self.open_time, self.tz, + open_offset) + self._closes = days_at_time( + _all_days, self.close_time, self.tz, close_offset + ) + + # `DatetimeIndex`s of nonstandard opens/closes + _special_opens = self._special_opens(start, end) + _special_closes = self._special_closes(start, end) + + # Overwrite the special opens and closes on top of the standard ones. + _overwrite_special_dates(_all_days, self._opens, _special_opens) + _overwrite_special_dates(_all_days, self._closes, _special_closes) + + # In pandas 0.16.1 _opens and _closes will lose their timezone + # information. This looks like it has been resolved in 0.17.1. + # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa + self.schedule = DataFrame( + index=_all_days, + columns=['market_open', 'market_close'], + data={ + 'market_open': self._opens, + 'market_close': self._closes, + }, + dtype='datetime64[ns]', + ) + + self.market_opens_nanos = self.schedule.market_open.values.\ + astype(np.int64) + + self.market_closes_nanos = self.schedule.market_close.values.\ + astype(np.int64) + + self._trading_minutes_nanos = self.all_minutes.values.\ + astype(np.int64) + + self.first_trading_session = _all_days[0] + self.last_trading_session = _all_days[-1] + + self._early_closes = pd.DatetimeIndex( + _special_closes.map(self.minute_to_session_label) + ) + + @property + def opens(self): + return self.schedule.market_open + + @property + def closes(self): + return self.schedule.market_close + + @property + def early_closes(self): + return self._early_closes + + def is_session(self, dt): + """ + Given a dt, returns whether it's a valid session label. + + Parameters + ---------- + dt: pd.Timestamp + The dt that is being tested. + + Returns + ------- + bool + Whether the given dt is a valid session label. + """ + return dt in self.schedule.index + + def is_open_on_minute(self, dt): + """ + Given a dt, return whether this exchange is open at the given dt. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to check if this exchange is open. + + Returns + ------- + bool + Whether the exchange is open on this dt. + """ + return is_open(self.market_opens_nanos, self.market_closes_nanos, + dt.value) + + def next_open(self, dt): + """ + Given a dt, returns the next open. + + If the given dt happens to be a session open, the next session's open + will be returned. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to get the next open. + + Returns + ------- + pd.Timestamp + The UTC timestamp of the next open. + """ + idx = next_divider_idx(self.market_opens_nanos, dt.value) + return self.schedule.market_open[idx].tz_localize('UTC') + + def next_close(self, dt): + """ + Given a dt, returns the next close. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to get the next close. + + Returns + ------- + pd.Timestamp + The UTC timestamp of the next close. + """ + idx = next_divider_idx(self.market_closes_nanos, dt.value) + return self.schedule.market_close[idx].tz_localize('UTC') + + def previous_open(self, dt): + """ + Given a dt, returns the previous open. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to get the previous open. + + Returns + ------- + pd.Timestamp + The UTC imestamp of the previous open. + """ + idx = previous_divider_idx(self.market_opens_nanos, dt.value) + return self.schedule.market_open[idx].tz_localize('UTC') + + def previous_close(self, dt): + """ + Given a dt, returns the previous close. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to get the previous close. + + Returns + ------- + pd.Timestamp + The UTC timestamp of the previous close. + """ + idx = previous_divider_idx(self.market_closes_nanos, dt.value) + return self.schedule.market_close[idx].tz_localize('UTC') + + def next_minute(self, dt): + """ + Given a dt, return the next exchange minute. If the given dt is not + an exchange minute, returns the next exchange open. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to get the next exchange minute. + + Returns + ------- + pd.Timestamp + The next exchange minute. + """ + idx = next_divider_idx(self._trading_minutes_nanos, dt.value) + return self.all_minutes[idx] + + def previous_minute(self, dt): + """ + Given a dt, return the previous exchange minute. + + Raises KeyError if the given timestamp is not an exchange minute. + + Parameters + ---------- + dt: pd.Timestamp + The dt for which to get the previous exchange minute. + + Returns + ------- + pd.Timestamp + The previous exchange minute. + """ + + idx = previous_divider_idx(self._trading_minutes_nanos, dt.value) + return self.all_minutes[idx] + + def next_session_label(self, session_label): + """ + Given a session label, returns the label of the next session. + + Parameters + ---------- + session_label: pd.Timestamp + A session whose next session is desired. + + Returns + ------- + pd.Timestamp + The next session label (midnight UTC). + + Notes + ----- + Raises ValueError if the given session is the last session in this + calendar. + """ + idx = self.schedule.index.get_loc(session_label) + try: + return self.schedule.index[idx + 1] + except IndexError: + if idx == len(self.schedule.index) - 1: + raise ValueError("There is no next session as this is the end" + " of the exchange calendar.") + else: + raise + + def previous_session_label(self, session_label): + """ + Given a session label, returns the label of the previous session. + + Parameters + ---------- + session_label: pd.Timestamp + A session whose previous session is desired. + + Returns + ------- + pd.Timestamp + The previous session label (midnight UTC). + + Notes + ----- + Raises ValueError if the given session is the first session in this + calendar. + """ + idx = self.schedule.index.get_loc(session_label) + if idx == 0: + raise ValueError("There is no previous session as this is the" + " beginning of the exchange calendar.") + + return self.schedule.index[idx - 1] + + def minutes_for_session(self, session_label): + """ + Given a session label, return the minutes for that session. + + Parameters + ---------- + session_label: pd.Timestamp (midnight UTC) + A session label whose session's minutes are desired. + + Returns + ------- + pd.DateTimeIndex + All the minutes for the given session. + """ + data = self.schedule.loc[session_label] + return self.all_minutes[ + self.all_minutes.slice_indexer( + data.market_open, + data.market_close + ) + ] + + def minutes_window(self, start_dt, count): + try: + start_idx = self.all_minutes.get_loc(start_dt) + except KeyError: + # if this is not a market minute, go to the previous session's + # close + previous_session = self.minute_to_session_label( + start_dt, direction="previous" + ) + + previous_close = self.open_and_close_for_session( + previous_session + )[1] + + start_idx = self.all_minutes.get_loc(previous_close) + + end_idx = start_idx + count + + if start_idx > end_idx: + return self.all_minutes[(end_idx + 1):(start_idx + 1)] + else: + return self.all_minutes[start_idx:end_idx] + + def sessions_in_range(self, start_session_label, end_session_label): + """ + Given start and end session labels, return all the sessions in that + range, inclusive. + + Parameters + ---------- + start_session_label: pd.Timestamp (midnight UTC) + The label representing the first session of the desired range. + + end_session_label: pd.Timestamp (midnight UTC) + The label representing the last session of the desired range. + + Returns + ------- + pd.DatetimeIndex + The desired sessions. + """ + return self.all_sessions[ + self.all_sessions.slice_indexer( + start_session_label, + end_session_label + ) + ] + + def sessions_window(self, session_label, count): + """ + Given a session label and a window size, returns a list of sessions + of size `count` + 1, that either starts with the given session + (if `count` is positive) or ends with the given session (if `count` is + negative). + + Parameters + ---------- + session_label: pd.Timestamp + The label of the initial session. + + count: int + Defines the length and the direction of the window. + + Returns + ------- + pd.DatetimeIndex + The desired sessions. + """ + start_idx = self.schedule.index.get_loc(session_label) + end_idx = start_idx + count + + return self.all_sessions[ + min(start_idx, end_idx):max(start_idx, end_idx) + 1 + ] + + def session_distance(self, start_session_label, end_session_label): + """ + Given a start and end session label, returns the distance between + them. For example, for three consecutive sessions Mon., Tues., and + Wed, `session_distance(Mon, Wed)` would return 2. + + Parameters + ---------- + start_session_label: pd.Timestamp + The label of the start session. + + end_session_label: pd.Timestamp + The label of the ending session. + + Returns + ------- + int + The distance between the two sessions. + """ + start_idx = self.all_sessions.searchsorted( + self.minute_to_session_label(start_session_label) + ) + + end_idx = self.all_sessions.searchsorted( + self.minute_to_session_label(end_session_label) + ) + + return abs(end_idx - start_idx) + + def minutes_in_range(self, start_minute, end_minute): + """ + Given start and end minutes, return all the calendar minutes + in that range, inclusive. + + Given minutes don't need to be calendar minutes. + + Parameters + ---------- + start_minute: pd.Timestamp + The minute representing the start of the desired range. + + end_minute: pd.Timestamp + The minute representing the end of the desired range. + + Returns + ------- + pd.DatetimeIndex + The minutes in the desired range. + """ + start_idx = searchsorted(self._trading_minutes_nanos, + start_minute.value) + + end_idx = searchsorted(self._trading_minutes_nanos, + end_minute.value) + + if end_minute.value == self._trading_minutes_nanos[end_idx]: + # if the end minute is a market minute, increase by 1 + end_idx += 1 + + return self.all_minutes[start_idx:end_idx] + + def minutes_for_sessions_in_range(self, start_session_label, + end_session_label): + """ + Returns all the minutes for all the sessions from the given start + session label to the given end session label, inclusive. + + Parameters + ---------- + start_session_label: pd.Timestamp + The label of the first session in the range. + + end_session_label: pd.Timestamp + The label of the last session in the range. + + Returns + ------- + pd.DatetimeIndex + The minutes in the desired range. + + """ + first_minute, _ = self.open_and_close_for_session(start_session_label) + _, last_minute = self.open_and_close_for_session(end_session_label) + + return self.minutes_in_range(first_minute, last_minute) + + def open_and_close_for_session(self, session_label): + """ + Returns a tuple of timestamps of the open and close of the session + represented by the given label. + + Parameters + ---------- + session_label: pd.Timestamp + The session whose open and close are desired. + + Returns + ------- + (Timestamp, Timestamp) + The open and close for the given session. + """ + o_and_c = self.schedule.loc[session_label] + + # `market_open` and `market_close` should be timezone aware, but pandas + # 0.16.1 does not appear to support this: + # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa + return (o_and_c['market_open'].tz_localize('UTC'), + o_and_c['market_close'].tz_localize('UTC')) + + @property + def all_sessions(self): + return self.schedule.index + + @property + def first_session(self): + return self.all_sessions[0] + + @property + def last_session(self): + return self.all_sessions[-1] + + @property + @remember_last + def all_minutes(self): + """ + Returns a DatetimeIndex representing all the minutes in this calendar. + """ + opens_in_ns = \ + self._opens.values.astype('datetime64[ns]') + + closes_in_ns = \ + self._closes.values.astype('datetime64[ns]') + + deltas = closes_in_ns - opens_in_ns + + # + 1 because we want 390 days per standard day, not 389 + daily_sizes = (deltas / NANOS_IN_MINUTE) + 1 + num_minutes = np.sum(daily_sizes).astype(np.int64) + + # One allocation for the entire thing. This assumes that each day + # represents a contiguous block of minutes. + all_minutes = np.empty(num_minutes, dtype='datetime64[ns]') + + idx = 0 + for day_idx, size in enumerate(daily_sizes): + # lots of small allocations, but it's fast enough for now. + + # size is a np.timedelta64, so we need to int it + size_int = int(size) + all_minutes[idx:(idx + size_int)] = \ + np.arange( + opens_in_ns[day_idx], + closes_in_ns[day_idx] + NANOS_IN_MINUTE, + NANOS_IN_MINUTE + ) + + idx += size_int + + return DatetimeIndex(all_minutes).tz_localize("UTC") + + def minute_to_session_label(self, dt, direction="next"): + """ + Given a minute, get the label of its containing session. + + Parameters + ---------- + dt : pd.Timestamp + The dt for which to get the containing session. + + direction: str + "next" (default) means that if the given dt is not part of a + session, return the label of the next session. + + "previous" means that if the given dt is not part of a session, + return the label of the previous session. + + "none" means that a KeyError will be raised if the given + dt is not part of a session. + + Returns + ------- + pd.Timestamp (midnight UTC) + The label of the containing session. + """ + + idx = searchsorted(self.market_closes_nanos, dt.value) + current_or_next_session = self.schedule.index[idx] + + if direction == "previous": + if not is_open(self.market_opens_nanos, self.market_closes_nanos, + dt.value): + # if the exchange is closed, use the previous session + return self.schedule.index[idx - 1] + elif direction == "none": + if not is_open(self.market_opens_nanos, self.market_closes_nanos, + dt.value): + # if the exchange is closed, blow up + raise ValueError("The given dt is not an exchange minute!") + elif direction != "next": + # invalid direction + raise ValueError("Invalid direction parameter: " + "{0}".format(direction)) + + return current_or_next_session + + def _special_dates(self, calendars, ad_hoc_dates, start_date, end_date): + """ + Union an iterable of pairs of the form (time, calendar) + and an iterable of pairs of the form (time, [dates]) + + (This is shared logic for computing special opens and special closes.) + """ + _dates = DatetimeIndex([], tz='UTC').union_many( + [ + holidays_at_time(calendar, start_date, end_date, time_, + self.tz) + for time_, calendar in calendars + ] + [ + days_at_time(datetimes, time_, self.tz) + for time_, datetimes in ad_hoc_dates + ] + ) + return _dates[(_dates >= start_date) & (_dates <= end_date)] + + def _special_opens(self, start, end): + return self._special_dates( + self.special_opens_calendars, + self.special_opens_adhoc, + start, + end, + ) + + def _special_closes(self, start, end): + return self._special_dates( + self.special_closes_calendars, + self.special_closes_adhoc, + start, + end, + ) + + +def days_at_time(days, t, tz, day_offset=0): + """ + Shift an index of days to time t, interpreted in tz. + + Overwrites any existing tz info on the input. + + Parameters + ---------- + days : DatetimeIndex + The "base" time which we want to change. + t : datetime.time + The time we want to offset @days by + tz : pytz.timezone + The timezone which these times represent + day_offset : int + The number of days we want to offset @days by + """ + days = DatetimeIndex(days).tz_localize(None).tz_localize(tz) + days_offset = days + DateOffset(day_offset) + return days_offset.shift( + 1, freq=DateOffset(hour=t.hour, minute=t.minute, second=t.second) + ).tz_convert('UTC') + + +def holidays_at_time(calendar, start, end, time, tz): + return days_at_time( + calendar.holidays( + # Workaround for https://github.com/pydata/pandas/issues/9825. + start.tz_localize(None), + end.tz_localize(None), + ), + time, + tz=tz, + ) + + +def _overwrite_special_dates(midnight_utcs, + opens_or_closes, + special_opens_or_closes): + """ + Overwrite dates in open_or_closes with corresponding dates in + special_opens_or_closes, using midnight_utcs for alignment. + """ + # Short circuit when nothing to apply. + if not len(special_opens_or_closes): + return + + len_m, len_oc = len(midnight_utcs), len(opens_or_closes) + if len_m != len_oc: + raise ValueError( + "Found misaligned dates while building calendar.\n" + "Expected midnight_utcs to be the same length as open_or_closes,\n" + "but len(midnight_utcs)=%d, len(open_or_closes)=%d" % len_m, len_oc + ) + + # Find the array indices corresponding to each special date. + indexer = midnight_utcs.get_indexer(special_opens_or_closes.normalize()) + + # -1 indicates that no corresponding entry was found. If any -1s are + # present, then we have special dates that doesn't correspond to any + # trading day. + if -1 in indexer: + bad_dates = list(special_opens_or_closes[indexer == -1]) + raise ValueError("Special dates %s are not trading days." % bad_dates) + + # NOTE: This is a slightly dirty hack. We're in-place overwriting the + # internal data of an Index, which is conceptually immutable. Since we're + # maintaining sorting, this should be ok, but this is a good place to + # sanity check if things start going haywire with calendar computations. + opens_or_closes.values[indexer] = special_opens_or_closes.values diff --git a/zipline/utils/calendars/trading_schedule.py b/zipline/utils/calendars/trading_schedule.py deleted file mode 100644 index f0aaa0f0..00000000 --- a/zipline/utils/calendars/trading_schedule.py +++ /dev/null @@ -1,416 +0,0 @@ -# -# Copyright 2016 Quantopian, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ( - ABCMeta, - abstractmethod, - abstractproperty, -) -from six import with_metaclass - -from .exchange_calendar import get_calendar -from .calendar_helpers import ( - next_scheduled_day, - previous_scheduled_day, - next_open_and_close, - previous_open_and_close, - scheduled_day_distance, - minutes_for_day, - days_in_range, - minutes_for_days_in_range, - add_scheduled_days, - all_scheduled_minutes, - next_scheduled_minute, - previous_scheduled_minute -) - - -class TradingSchedule(with_metaclass(ABCMeta)): - """ - A TradingSchedule defines the execution timing of a TradingAlgorithm. - """ - - def next_execution_day(self, date): - return next_scheduled_day( - date, - last_trading_day=self.last_execution_day, - is_scheduled_day_hook=self.is_executing_on_day, - ) - - def previous_execution_day(self, date): - return previous_scheduled_day( - date, - first_trading_day=self.first_execution_day, - is_scheduled_day_hook=self.is_executing_on_day, - ) - - def next_start_and_end(self, date): - return next_open_and_close( - date, - open_and_close_hook=self.start_and_end, - next_scheduled_day_hook=self.next_execution_day, - ) - - def previous_start_and_end(self, date): - return previous_open_and_close( - date, - open_and_close_hook=self.start_and_end, - previous_scheduled_day_hook=self.previous_execution_day, - ) - - def execution_day_distance(self, first_date, second_date): - return scheduled_day_distance( - first_date, second_date, - all_days=self.all_execution_days, - ) - - def execution_minutes_for_day(self, day): - return minutes_for_day( - day, - open_and_close_hook=self.start_and_end, - ) - - def execution_days_in_range(self, start, end): - return days_in_range( - start, end, - all_days=self.all_execution_days, - ) - - def execution_minutes_for_days_in_range(self, start, end): - return minutes_for_days_in_range( - start, end, - days_in_range_hook=self.execution_days_in_range, - minutes_for_day_hook=self.execution_minutes_for_day, - ) - - def add_execution_days(self, n, date): - """ - Adds n execution days to date. If this would fall outside of the - TradingSchedule, a NoFurtherDataError is raised. - - Parameters - ---------- - n : int - The number of days to add to date, this can be positive or - negative. - date : datetime - The date to add to. - - Returns - ------- - datetime - n trading days added to date. - """ - return add_scheduled_days( - n, date, - next_scheduled_day_hook=self.next_execution_day, - previous_scheduled_day_hook=self.previous_execution_day, - all_trading_days=self.all_execution_days, - ) - - def next_execution_minute(self, start): - return next_scheduled_minute( - start, - is_scheduled_day_hook=self.is_executing_on_day, - open_and_close_hook=self.start_and_end, - next_open_and_close_hook=self.next_start_and_end, - ) - - def previous_execution_minute(self, start): - return previous_scheduled_minute( - start, - is_scheduled_day_hook=self.is_executing_on_day, - open_and_close_hook=self.start_and_end, - previous_open_and_close_hook=self.previous_start_and_end, - ) - - def execution_minute_window(self, start, count): - start_idx = self.all_execution_minutes.get_loc(start) - end_idx = start_idx + count - - if start_idx > end_idx: - return self.all_execution_minutes[(end_idx + 1):(start_idx + 1)] - else: - return self.all_execution_minutes[start_idx:end_idx] - - @abstractproperty - def day(self): - """ - A CustomBusinessDay defining those days on which the algorithm is - trading. - """ - raise NotImplementedError() - - @abstractproperty - def tz(self): - """ - The native timezone for this TradingSchedule. - """ - raise NotImplementedError() - - @abstractproperty - def first_execution_day(self): - """ - The first possible day of trading in this TradingSchedule. - """ - raise NotImplementedError() - - @abstractproperty - def last_execution_day(self): - """ - The last possible day of trading in this TradingSchedule. - """ - raise NotImplementedError() - - @abstractmethod - def trading_sessions(self, start, end): - """ - Calculates all of the trading sessions between the given - start and end. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DataFrame - A DataFrame, with a DatetimeIndex of trading dates, containing - columns of trading starts and ends in this TradingSchedule. - """ - raise NotImplementedError() - - @property - def all_execution_days(self): - return self.schedule.index - - @property - def all_execution_minutes(self): - return all_scheduled_minutes(self.all_execution_days, - self.execution_minutes_for_days_in_range) - - def trading_dates(self, start, end): - """ - Calculates the dates of all of the trading sessions between the given - start and end. - - Parameters - ---------- - start : Timestamp - end : Timestamp - - Returns - ------- - DatetimeIndex - A DatetimeIndex containing the dates of the desired trading - sessions. - """ - return self.trading_sessions(start, end).index - - @abstractmethod - def data_availability_time(self, date): - """ - Given a UTC-canonicalized date, returns a time by-which all data from - the previous date is available to the algorithm. - - Parameters - ---------- - date : Timestamp - The UTC-canonicalized calendar date whose data availability time - is needed. - - Returns - ------- - Timestamp or None - The data availability time on the given date, or None if there is - no data availability time for that date. - """ - raise NotImplementedError() - - @abstractmethod - def start_and_end(self, date): - """ - Given a UTC-canonicalized date, returns a tuple of timestamps of the - start and end of the algorithm trading session for that date. - - Parameters - ---------- - date : Timestamp - The UTC-canonicalized algorithm trading session date whose start - and end are needed. - - Returns - ------- - (Timestamp, Timestamp) - The start and end for the given date. - """ - raise NotImplementedError() - - @abstractmethod - def is_executing_on_minute(self, dt): - """ - Calculates if a TradingAlgorithm using this TradingSchedule should be - executed at time dt. - - Parameters - ---------- - dt : Timestamp - The time being queried. - - Returns - ------- - bool - True if the TradingAlgorithm should be executed at dt, - otherwise False. - """ - raise NotImplementedError() - - @abstractmethod - def is_executing_on_day(self, dt): - """ - Calculates if a TradingAlgorithm using this TradingSchedule would - execute on the day of dt. - - Parameters - ---------- - dt : Timestamp - The time being queried. - - Returns - ------- - bool - True if the TradingAlgorithm should be executed at dt, - otherwise False. - """ - raise NotImplementedError() - - @abstractmethod - def session_date(self, dt): - """ - 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 - ---------- - dt : Timestamp - - Returns - ------- - Timestamp - The date of the exchange session in which dt belongs. - """ - raise NotImplementedError() - - @abstractproperty - def early_ends(self): - """ - Returns a DatetimeIndex containing the session dates on-which there is - an early end to trading. - """ - raise NotImplementedError() - - -class ExchangeTradingSchedule(TradingSchedule): - """ - A TradingSchedule that functions as a wrapper around an ExchangeCalendar. - """ - - def __init__(self, cal): - """ - Docstring goes here, Jimmy - - Parameters - ---------- - cal : ExchangeCalendar - The ExchangeCalendar to be represented by this - ExchangeTradingSchedule. - """ - self._exchange_calendar = cal - super(ExchangeTradingSchedule, self).__init__() - - @property - def all_execution_days(self): - return self._exchange_calendar.all_trading_days - - @property - def all_execution_minutes(self): - return self._exchange_calendar.all_trading_minutes - - @property - def day(self): - return self._exchange_calendar.day - - @property - def tz(self): - return self._exchange_calendar.tz - - @property - def schedule(self): - return self._exchange_calendar.schedule - - @property - def first_execution_day(self): - return self._exchange_calendar.first_trading_day - - @property - def last_execution_day(self): - return self._exchange_calendar.last_trading_day - - def trading_sessions(self, start, end): - """ - See TradingSchedule definition. - """ - return self._exchange_calendar.trading_days(start, end) - - def data_availability_time(self, date): - """ - See TradingSchedule definition. - """ - calendar_open, _ = self._exchange_calendar.open_and_close(date) - return calendar_open - - def start_and_end(self, date): - """ - See TradingSchedule definition. - """ - return self._exchange_calendar.open_and_close(date) - - def is_executing_on_minute(self, dt): - """ - See TradingSchedule definition. - """ - return self._exchange_calendar.is_open_on_minute(dt) - - def is_executing_on_day(self, dt): - """ - See TradingSchedule definition. - """ - return self._exchange_calendar.is_open_on_day(dt) - - def session_date(self, dt): - """ - See TradingSchedule definition. - """ - return self._exchange_calendar.session_date(dt) - - @property - def early_ends(self): - return self._exchange_calendar.early_closes - - -default_nyse_schedule = ExchangeTradingSchedule(cal=get_calendar('NYSE')) diff --git a/zipline/utils/calendars/us_holidays.py b/zipline/utils/calendars/us_holidays.py new file mode 100644 index 00000000..afaf40f5 --- /dev/null +++ b/zipline/utils/calendars/us_holidays.py @@ -0,0 +1,147 @@ +from pandas import ( + Timestamp, + DateOffset, + date_range, +) + +from pandas.tseries.holiday import ( + Holiday, + sunday_to_monday, + nearest_workday, +) + +from dateutil.relativedelta import ( + MO, + TH +) +from pandas.tseries.offsets import Day + +from zipline.utils.calendars.trading_calendar import ( + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, +) + +# These have the same definition, but are used in different places because the +# NYSE closed at 2:00 PM on Christmas Eve until 1993. +from zipline.utils.pandas_utils import july_5th_holiday_observance + +ChristmasEveBefore1993 = Holiday( + 'Christmas Eve', + month=12, + day=24, + end_date=Timestamp('1993-01-01'), + # When Christmas is a Saturday, the 24th is a full holiday. + days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY), +) +ChristmasEveInOrAfter1993 = Holiday( + 'Christmas Eve', + month=12, + day=24, + start_date=Timestamp('1993-01-01'), + # When Christmas is a Saturday, the 24th is a full holiday. + days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY), +) +USNewYearsDay = Holiday( + 'New Years Day', + month=1, + day=1, + # When Jan 1 is a Sunday, US markets observe the subsequent Monday. + # When Jan 1 is a Saturday (as in 2005 and 2011), no holiday is observed. + observance=sunday_to_monday +) +USMartinLutherKingJrAfter1998 = Holiday( + 'Dr. Martin Luther King Jr. Day', + month=1, + day=1, + # The US markets didn't observe MLK day as a holiday until 1998. + start_date=Timestamp('1998-01-01'), + offset=DateOffset(weekday=MO(3)), +) +USMemorialDay = Holiday( + # NOTE: The definition for Memorial Day is incorrect as of pandas 0.16.0. + # See https://github.com/pydata/pandas/issues/9760. + 'Memorial Day', + month=5, + day=25, + offset=DateOffset(weekday=MO(1)), +) +USIndependenceDay = Holiday( + 'July 4th', + month=7, + day=4, + observance=nearest_workday, +) +Christmas = Holiday( + 'Christmas', + month=12, + day=25, + observance=nearest_workday, +) + +MonTuesThursBeforeIndependenceDay = Holiday( + # When July 4th is a Tuesday, Wednesday, or Friday, the previous day is a + # half day. + 'Mondays, Tuesdays, and Thursdays Before Independence Day', + month=7, + day=3, + days_of_week=(MONDAY, TUESDAY, THURSDAY), + start_date=Timestamp("1995-01-01"), +) +FridayAfterIndependenceDayExcept2013 = Holiday( + # When July 4th is a Thursday, the next day is a half day (except in 2013, + # when, for no explicable reason, Wednesday was a half day instead). + "Fridays after Independence Day that aren't in 2013", + month=7, + day=5, + days_of_week=(FRIDAY,), + observance=july_5th_holiday_observance, + start_date=Timestamp("1995-01-01"), +) +USBlackFridayBefore1993 = Holiday( + 'Black Friday', + month=11, + day=1, + # Black Friday was not observed until 1992. + start_date=Timestamp('1992-01-01'), + end_date=Timestamp('1993-01-01'), + offset=[DateOffset(weekday=TH(4)), Day(1)], +) +USBlackFridayInOrAfter1993 = Holiday( + 'Black Friday', + month=11, + day=1, + start_date=Timestamp('1993-01-01'), + offset=[DateOffset(weekday=TH(4)), Day(1)], +) +BattleOfGettysburg = Holiday( + # All of the floor traders in Chicago were sent to PA + 'Markets were closed during the battle of Gettysburg', + month=7, + day=(1, 2, 3), + start_date=Timestamp("1863-07-01"), + end_date=Timestamp("1863-07-03") +) + + +# http://en.wikipedia.org/wiki/Aftermath_of_the_September_11_attacks +September11Closings = date_range('2001-09-11', '2001-09-16', tz='UTC') + +# http://en.wikipedia.org/wiki/Hurricane_sandy +HurricaneSandyClosings = date_range( + '2012-10-29', + '2012-10-30', + tz='UTC' +) + +# National Days of Mourning +# - President Richard Nixon - April 27, 1994 +# - President Ronald W. Reagan - June 11, 2004 +# - President Gerald R. Ford - Jan 2, 2007 +USNationalDaysofMourning = [ + Timestamp('1994-04-27', tz='UTC'), + Timestamp('2004-06-11', tz='UTC'), + Timestamp('2007-01-02', tz='UTC'), +] diff --git a/zipline/utils/events.py b/zipline/utils/events.py index d3c6a484..abdc3506 100644 --- a/zipline/utils/events.py +++ b/zipline/utils/events.py @@ -1,5 +1,5 @@ # -# Copyright 2014 Quantopian, Inc. +# Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,10 +20,9 @@ import datetime import pandas as pd import pytz +from zipline.utils.memoize import lazyval from .context_tricks import nop_context -from zipline.errors import NoFurtherDataError -from zipline.utils.calendars import normalize_date __all__ = [ 'EventManager', @@ -50,7 +49,7 @@ __all__ = [ ] -MAX_MONTH_RANGE = 26 +MAX_MONTH_RANGE = 23 MAX_WEEK_RANGE = 5 @@ -320,7 +319,10 @@ class AfterOpen(StatelessRule): def calculate_dates(self, dt): # given a dt, find that day's open and period end (open + offset) - self._period_start, self._period_close = self.cal.open_and_close(dt) + self._period_start, self._period_close = \ + self.cal.open_and_close_for_session( + self.cal.minute_to_session_label(dt, direction="none") + ) self._period_end = self._period_start + self.offset - self._one_minute def should_trigger(self, dt): @@ -358,13 +360,18 @@ class BeforeClose(StatelessRule): ) self._period_start = None + self._period_close = None self._period_end = None self._one_minute = datetime.timedelta(minutes=1) def calculate_dates(self, dt): # given a dt, find that day's close and period start (close - offset) - self._period_end = self.cal.open_and_close(dt)[1] + self._period_end = \ + self.cal.open_and_close_for_session( + self.cal.minute_to_session_label(dt) + )[1] + self._period_start = self._period_end - self.offset self._period_close = self._period_end @@ -378,10 +385,7 @@ class BeforeClose(StatelessRule): # that we will NOT correctly recognize a new date if we go backwards # in time(which should never happen in a simulation, or in live # trading) - if ( - self._period_start is None or - self._period_close <= dt - ): + if self._period_start is None or self._period_close <= dt: self.calculate_dates(dt) return self._period_start == dt @@ -392,59 +396,28 @@ class NotHalfDay(StatelessRule): A rule that only triggers when it is not a half day. """ def should_trigger(self, dt): - return normalize_date(dt) not in self.cal.early_closes + return self.cal.minute_to_session_label(dt, direction="none") \ + not in self.cal.early_closes class TradingDayOfWeekRule(six.with_metaclass(ABCMeta, StatelessRule)): def __init__(self, n, invert): if not 0 <= n < MAX_WEEK_RANGE: raise _out_of_range_error(MAX_WEEK_RANGE) - self.next_date_start = None - self.next_date_end = None - self.next_midnight_timestamp = None - self.td_delta = -n if invert else n - @abstractmethod - def date_func(self, dt, cal): - raise NotImplementedError + self.td_delta = (-n - 1) if invert else n - def calculate_start_and_end(self, dt): - while True: - next_trading_day = self.cal.add_trading_days( - self.td_delta, - self.date_func(dt, self.cal), - ) - - # If after applying the offset to the start/end day of the week, we - # get day in a different week, skip this week and go on to the next - if next_trading_day.isocalendar()[1] == dt.isocalendar()[1]: - break - else: - dt += datetime.timedelta(days=7) - - next_open, next_close = self.cal.open_and_close(next_trading_day) - self.next_date_start = next_open - self.next_date_end = next_close - self.next_midnight_timestamp = next_trading_day + @lazyval + def execution_periods(self): + # calculate the list of periods that match the given criteria + return self.cal.schedule.groupby( + pd.Grouper(freq="W") + ).nth(self.td_delta).index def should_trigger(self, dt): - if self.next_date_start is None: - # First time this method has been called. Calculate the midnight, - # open, and close for the first trigger, which occurs on the week - # of the simulation start - self.calculate_start_and_end(dt) - - # If we've passed the trigger, calculate the next one - if dt > self.next_date_end: - self.calculate_start_and_end(self.next_date_end + - datetime.timedelta(days=7)) - - # if the given dt is within the next matching day, return true. - if self.next_date_start <= dt <= self.next_date_end or \ - dt == self.next_midnight_timestamp: - return True - - return False + # is this market minute's period in the list of execution periods? + return self.cal.minute_to_session_label(dt, direction="none") in \ + self.execution_periods class NthTradingDayOfWeek(TradingDayOfWeekRule): @@ -455,25 +428,6 @@ class NthTradingDayOfWeek(TradingDayOfWeekRule): def __init__(self, n): super(NthTradingDayOfWeek, self).__init__(n, invert=False) - @staticmethod - def get_first_trading_day_of_week(dt, cal): - prev = None - # Traverse backward until we hit a week border, then jump back to the - # previous trading day. - try: - while not prev or dt.weekday() < prev.weekday(): - prev = dt - dt = cal.previous_trading_day(dt) - except NoFurtherDataError: - prev = dt - - if cal.is_open_on_day(prev): - return prev - else: - return cal.next_trading_day(prev) - - date_func = get_first_trading_day_of_week - class NDaysBeforeLastTradingDayOfWeek(TradingDayOfWeekRule): """ @@ -482,51 +436,27 @@ class NDaysBeforeLastTradingDayOfWeek(TradingDayOfWeekRule): def __init__(self, n): super(NDaysBeforeLastTradingDayOfWeek, self).__init__(n, invert=True) - @staticmethod - def get_last_trading_day_of_week(dt, cal): - prev = None - # Traverse forward until we hit a week border, then jump back to the - # previous trading day. - try: - while not prev or dt.weekday() > prev.weekday(): - prev = dt - dt = cal.next_trading_day(dt) - except NoFurtherDataError: - prev = dt - - if cal.is_open_on_day(prev): - return prev - else: - return cal.previous_trading_day(prev) - - date_func = get_last_trading_day_of_week - class TradingDayOfMonthRule(six.with_metaclass(ABCMeta, StatelessRule)): def __init__(self, n, invert): if not 0 <= n < MAX_MONTH_RANGE: raise _out_of_range_error(MAX_MONTH_RANGE) - self.month = None - self.date = None - self.td_delta = -n if invert else n + if invert: + self.td_delta = -n - 1 + else: + self.td_delta = n def should_trigger(self, dt): - return self.get_trigger_day_of_month(dt) == normalize_date(dt) + # is this market minute's period in the list of execution periods? + return self.cal.minute_to_session_label(dt, direction="none") in \ + self.execution_periods - @abstractmethod - def date_func(self, dt): - raise NotImplementedError - - def get_trigger_day_of_month(self, dt): - if self.month == dt.month: - # We already computed the day for this month. - return self.date - - self.date = self.date_func(dt) - if self.td_delta: - self.date = self.cal.add_trading_days(self.td_delta, self.date) - - return self.date + @lazyval + def execution_periods(self): + # calculate the list of periods that match the given criteria + return self.cal.schedule.groupby( + pd.Grouper(freq="M") + ).nth(self.td_delta).index class NthTradingDayOfMonth(TradingDayOfMonthRule): @@ -537,16 +467,6 @@ class NthTradingDayOfMonth(TradingDayOfMonthRule): def __init__(self, n): super(NthTradingDayOfMonth, self).__init__(n, invert=False) - def get_first_trading_day_of_month(self, dt): - self.month = dt.month - - dt = dt.replace(day=1) - first_day = (normalize_date(dt) if self.cal.is_open_on_day(dt) - else self.cal.next_trading_day(dt)) - return first_day - - date_func = get_first_trading_day_of_month - class NDaysBeforeLastTradingDayOfMonth(TradingDayOfMonthRule): """ @@ -555,25 +475,6 @@ class NDaysBeforeLastTradingDayOfMonth(TradingDayOfMonthRule): def __init__(self, n): super(NDaysBeforeLastTradingDayOfMonth, self).__init__(n, invert=True) - def get_last_trading_day_of_month(self, dt): - self.month = dt.month - - if dt.month == 12: - # Roll the year forward and start in January. - year = dt.year + 1 - month = 1 - else: - # Increment the month in the same year. - year = dt.year - month = dt.month + 1 - - last_day = self.cal.previous_trading_day( - dt.replace(year=year, month=month, day=1) - ) - return last_day - - date_func = get_last_trading_day_of_month - # Stateful rules diff --git a/zipline/utils/factory.py b/zipline/utils/factory.py index d42780d1..805213f6 100644 --- a/zipline/utils/factory.py +++ b/zipline/utils/factory.py @@ -1,5 +1,5 @@ # -# Copyright 2013 Quantopian, Inc. +# Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ Factory functions to prepare useful data. """ import pandas as pd import numpy as np -from datetime import timedelta +from datetime import timedelta, datetime from zipline.protocol import Event, DATASOURCE_TYPE from zipline.sources import SpecificEquityTrades @@ -29,7 +29,7 @@ from zipline.data.loader import ( # For backwards compatibility load_from_yahoo, load_bars_from_yahoo, ) -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar __all__ = ['load_from_yahoo', 'load_bars_from_yahoo'] @@ -40,49 +40,56 @@ def create_simulation_parameters(year=2006, start=None, end=None, num_days=None, data_frequency='daily', emission_rate='daily', - trading_schedule=default_nyse_schedule): + trading_calendar=None): + + if not trading_calendar: + trading_calendar = get_calendar("NYSE") + if start is None: start = pd.Timestamp("{0}-01-01".format(year), tz='UTC') + elif type(start) == datetime: + start = pd.Timestamp(start) + if end is None: if num_days: - start_index = trading_schedule.all_execution_days\ - .searchsorted(start) - end = trading_schedule.all_execution_days[ - start_index + num_days - 1 - ] + start_index = trading_calendar.all_sessions.searchsorted(start) + end = trading_calendar.all_sessions[start_index + num_days - 1] else: end = pd.Timestamp("{0}-12-31".format(year), tz='UTC') + elif type(end) == datetime: + end = pd.Timestamp(end) + sim_params = SimulationParameters( - period_start=start, - period_end=end, + start_session=start, + end_session=end, capital_base=capital_base, data_frequency=data_frequency, emission_rate=emission_rate, - trading_schedule=trading_schedule, + trading_calendar=trading_calendar, ) return sim_params -def get_next_trading_dt(current, interval, trading_schedule): - next_dt = pd.Timestamp(current).tz_convert(trading_schedule.tz) +def get_next_trading_dt(current, interval, trading_calendar): + next_dt = pd.Timestamp(current).tz_convert(trading_calendar.tz) while True: # Convert timestamp to naive before adding day, otherwise the when # stepping over EDT an hour is added. next_dt = pd.Timestamp(next_dt.replace(tzinfo=None)) next_dt = next_dt + interval - next_dt = pd.Timestamp(next_dt, tz=trading_schedule.tz) + next_dt = pd.Timestamp(next_dt, tz=trading_calendar.tz) next_dt_utc = next_dt.tz_convert('UTC') - if trading_schedule.is_executing_on_minute(next_dt_utc): + if trading_calendar.is_open_on_minute(next_dt_utc): break - next_dt = next_dt_utc.tz_convert(trading_schedule.tz) + next_dt = next_dt_utc.tz_convert(trading_calendar.tz) return next_dt_utc def create_trade_history(sid, prices, amounts, interval, sim_params, - trading_schedule, source_id="test_factory"): + trading_calendar, source_id="test_factory"): trades = [] current = sim_params.first_open @@ -95,7 +102,7 @@ def create_trade_history(sid, prices, amounts, interval, sim_params, trade_dt = current trade = create_trade(sid, price, amount, trade_dt, source_id) trades.append(trade) - current = get_next_trading_dt(current, interval, trading_schedule) + current = get_next_trading_dt(current, interval, trading_calendar) assert len(trades) == len(prices) return trades @@ -156,12 +163,12 @@ def create_txn(sid, price, amount, datetime): def create_txn_history(sid, priceList, amtList, interval, sim_params, - trading_schedule): + trading_calendar): txns = [] current = sim_params.first_open for price, amount in zip(priceList, amtList): - current = get_next_trading_dt(current, interval, trading_schedule) + current = get_next_trading_dt(current, interval, trading_calendar) txns.append(create_txn(sid, price, amount, current)) current = current + interval @@ -169,20 +176,20 @@ def create_txn_history(sid, priceList, amtList, interval, sim_params, def create_returns_from_range(sim_params): - return pd.Series(index=sim_params.trading_days, - data=np.random.rand(len(sim_params.trading_days))) + return pd.Series(index=sim_params.sessions, + data=np.random.rand(len(sim_params.sessions))) def create_returns_from_list(returns, sim_params): - return pd.Series(index=sim_params.trading_days[:len(returns)], + return pd.Series(index=sim_params.sessions[:len(returns)], data=returns) -def create_daily_trade_source(sids, sim_params, env, trading_schedule, +def create_daily_trade_source(sids, sim_params, env, trading_calendar, concurrent=False): """ creates trade_count trades for each sid in sids list. - first trade will be on sim_params.period_start, and daily + first trade will be on sim_params.start_session, and daily thereafter for each sid. Thus, two sids should result in two trades per day. """ @@ -191,19 +198,19 @@ def create_daily_trade_source(sids, sim_params, env, trading_schedule, timedelta(days=1), sim_params, env=env, - trading_schedule=trading_schedule, + trading_calendar=trading_calendar, concurrent=concurrent, ) def create_trade_source(sids, trade_time_increment, sim_params, env, - trading_schedule, concurrent=False): + trading_calendar, concurrent=False): # If the sim_params define an end that is during market hours, that will be # used as the end of the data source - if trading_schedule.is_executing_on_minute(sim_params.period_end): - end = sim_params.period_end - # Otherwise, the last_close after the period_end is used as the end of the + if trading_calendar.is_open_on_minute(sim_params.end_session): + end = sim_params.end_session + # Otherwise, the last_close after the end_session is used as the end of the # data source else: end = sim_params.last_close @@ -217,7 +224,7 @@ def create_trade_source(sids, trade_time_increment, sim_params, env, 'filter': sids, 'concurrent': concurrent, 'env': env, - 'trading_schedule': trading_schedule, + 'trading_calendar': trading_calendar, } source = SpecificEquityTrades(*args, **kwargs) diff --git a/zipline/utils/run_algo.py b/zipline/utils/run_algo.py index 1d202e3b..8c590737 100644 --- a/zipline/utils/run_algo.py +++ b/zipline/utils/run_algo.py @@ -20,7 +20,7 @@ from zipline.data.data_portal import DataPortal from zipline.finance.trading import TradingEnvironment from zipline.pipeline.data import USEquityPricing from zipline.pipeline.loaders import USEquityPricingLoader -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar import zipline.utils.paths as pth @@ -132,7 +132,7 @@ def _run(handle_data, first_trading_day =\ bundle_data.equity_minute_bar_reader.first_trading_day data = DataPortal( - env.asset_finder, default_nyse_schedule, + env.asset_finder, get_calendar("NYSE"), first_trading_day=first_trading_day, equity_minute_reader=bundle_data.equity_minute_bar_reader, equity_daily_reader=bundle_data.equity_daily_bar_reader, diff --git a/zipline/utils/simfactory.py b/zipline/utils/simfactory.py index ad03e79b..be814494 100644 --- a/zipline/utils/simfactory.py +++ b/zipline/utils/simfactory.py @@ -2,7 +2,7 @@ import zipline.utils.factory as factory from zipline.testing.core import create_data_portal_from_trade_history from zipline.test_algorithms import TestAlgorithm -from zipline.utils.calendars import default_nyse_schedule +from zipline.utils.calendars import get_calendar def create_test_zipline(**config): @@ -40,7 +40,7 @@ def create_test_zipline(**config): concurrent_trades = config.get('concurrent_trades', False) order_count = config.get('order_count', 100) order_amount = config.get('order_amount', 100) - trading_schedule = config.get('trading_schedule', default_nyse_schedule) + trading_calendar = config.get('trading_calendar', get_calendar("NYSE")) # ------------------- # Create the Algo @@ -54,7 +54,7 @@ def create_test_zipline(**config): order_count, sim_params=config.get('sim_params', factory.create_simulation_parameters()), - trading_schedule=trading_schedule, + trading_calendar=trading_calendar, slippage=config.get('slippage'), identifiers=sid_list ) @@ -70,7 +70,7 @@ def create_test_zipline(**config): sid_list, test_algo.sim_params, test_algo.trading_environment, - trading_schedule, + trading_calendar, concurrent=concurrent_trades, ) @@ -83,7 +83,7 @@ def create_test_zipline(**config): data_portal = create_data_portal_from_trade_history( config['env'].asset_finder, - trading_schedule, + trading_calendar, config['tempdir'], config['sim_params'], trades_by_sid