diff --git a/tests/test_history.py b/tests/test_history.py index 8745d661..a6357ce3 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -1761,6 +1761,64 @@ class DailyEquityHistoryTestCase(WithHistory, ZiplineTestCase): np.testing.assert_almost_equal(window_1[self.ASSET2].values, window_2[self.ASSET2].values) + def test_history_window_out_of_order_dates(self): + """ + Use a history window with non-monotonically increasing dates. + A scenario which does not occur during simulations, but useful + for using a history loader in a notebook. + """ + + window_1 = self.data_portal.get_history_window( + [self.ASSET1], + pd.Timestamp('2014-02-07', tz='UTC'), + 4, + "1d", + "close" + ) + + window_2 = self.data_portal.get_history_window( + [self.ASSET1], + pd.Timestamp('2014-02-05', tz='UTC'), + 4, + "1d", + "close" + ) + + window_3 = self.data_portal.get_history_window( + [self.ASSET1], + pd.Timestamp('2014-02-07', tz='UTC'), + 4, + "1d", + "close" + ) + + window_4 = self.data_portal.get_history_window( + [self.ASSET1], + pd.Timestamp('2014-01-22', tz='UTC'), + 4, + "1d", + "close" + ) + + # Calling 02-07 after resetting the window should not affect the + # results. + np.testing.assert_almost_equal(window_1.values, window_3.values) + + offsets = np.arange(4) + + def assert_window_prices(window, starting_price): + np.testing.assert_almost_equal(window.loc[:, self.ASSET1], + starting_price + offsets) + + # Window 1 starts on the 23rd day of data for ASSET 1. + assert_window_prices(window_1, 23) + # Window 2 starts on the 21st day of data for ASSET 1. + assert_window_prices(window_2, 21) + # Window 3 starts on the 23rd day of data for ASSET 1. + assert_window_prices(window_3, 23) + # Window 4 starts on the 11th day of data for ASSET 1. + assert_window_prices(window_4, 11) + class NoPrefetchDailyEquityHistoryTestCase(DailyEquityHistoryTestCase): DATA_PORTAL_MINUTE_HISTORY_PREFETCH = 0 diff --git a/zipline/data/history_loader.py b/zipline/data/history_loader.py index 40efde29..61fd0a38 100644 --- a/zipline/data/history_loader.py +++ b/zipline/data/history_loader.py @@ -379,12 +379,26 @@ class HistoryLoader(with_metaclass(ABCMeta)): assets = self._asset_finder.retrieve_all(assets) + try: + end_ix = self._calendar.get_loc(end) + except KeyError: + raise KeyError("{0} not in calendar [{1}...{2}]".format( + end, self._calendar[0], self._calendar[-1])) + for asset in assets: try: - asset_windows[asset] = self._window_blocks[field].get( + window = self._window_blocks[field].get( (asset, size, is_perspective_after), end) except KeyError: needed_assets.append(asset) + else: + if end_ix < window.most_recent_ix: + # Window needs reset. Requested end index occurs before the + # end index from the previous history call for this window. + # Grab new window instead of rewinding adjustments. + needed_assets.append(asset) + else: + asset_windows[asset] = window if needed_assets: start = dts[0] @@ -395,11 +409,6 @@ class HistoryLoader(with_metaclass(ABCMeta)): except KeyError: raise KeyError("{0} not in calendar [{1}...{2}]".format( start, self._calendar[0], self._calendar[-1])) - try: - end_ix = self._calendar.get_loc(end) - except KeyError: - raise KeyError("{0} not in calendar [{1}...{2}]".format( - end, self._calendar[0], self._calendar[-1])) cal = self._calendar prefetch_end_ix = min(end_ix + self._prefetch_length, len(cal) - 1) prefetch_end = cal[prefetch_end_ix]