diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 71df3863..1b754e02 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -4294,6 +4294,7 @@ class TestOrderAfterDelist(WithTradingEnvironment, ZiplineTestCase): def make_equity_info(cls): return pd.DataFrame.from_dict( { + # Asset whose auto close date is after its end date. 1: { 'start_date': cls.start, 'end_date': cls.day_1, @@ -4301,6 +4302,14 @@ class TestOrderAfterDelist(WithTradingEnvironment, ZiplineTestCase): 'symbol': "ASSET1", 'exchange': "TEST", }, + # Asset whose auto close date is before its end date. + 2: { + 'start_date': cls.start, + 'end_date': cls.day_4, + 'auto_close_date': cls.day_1, + 'symbol': 'ASSET2', + 'exchange': 'TEST', + }, }, orient='index', ) @@ -4310,7 +4319,13 @@ class TestOrderAfterDelist(WithTradingEnvironment, ZiplineTestCase): super(TestOrderAfterDelist, cls).init_class_fixtures() cls.data_portal = FakeDataPortal(cls.env) - def test_order_in_quiet_period(self): + @parameterized.expand([ + ('auto_close_after_end_date', 1), + ('auto_close_before_end_date', 2), + ]) + def test_order_in_quiet_period(self, name, sid): + asset = self.asset_finder.retrieve_asset(sid) + algo_code = dedent(""" from zipline.api import ( sid, @@ -4326,13 +4341,13 @@ class TestOrderAfterDelist(WithTradingEnvironment, ZiplineTestCase): pass def handle_data(context, data): - order(sid(1), 1) - order_value(sid(1), 100) - order_percent(sid(1), 0.5) - order_target(sid(1), 50) - order_target_percent(sid(1), 0.5) - order_target_value(sid(1), 50) - """) + order(sid({sid}), 1) + order_value(sid({sid}), 100) + order_percent(sid({sid}), 0.5) + order_target(sid({sid}), 50) + order_target_percent(sid({sid}), 0.5) + order_target_value(sid({sid}), 50) + """).format(sid=sid) # run algo from 1/6 to 1/7 algo = TradingAlgorithm( @@ -4356,11 +4371,12 @@ class TestOrderAfterDelist(WithTradingEnvironment, ZiplineTestCase): self.assertEqual(6 * 390, len(warnings)) for w in warnings: - self.assertEqual("Cannot place order for ASSET1, as it has " - "de-listed. Any existing positions for this " - "asset will be liquidated on " - "2016-01-11 00:00:00+00:00.", - w.message) + expected_message = ( + 'Cannot place order for ASSET{sid}, as it has de-listed. ' + 'Any existing positions for this asset will be liquidated ' + 'on {date}.'.format(sid=sid, date=asset.auto_close_date) + ) + self.assertEqual(expected_message, w.message) class AlgoInputValidationTestCase(ZiplineTestCase): diff --git a/tests/test_bar_data.py b/tests/test_bar_data.py index d38f1fbd..bf13540d 100644 --- a/tests/test_bar_data.py +++ b/tests/test_bar_data.py @@ -716,9 +716,9 @@ class TestMinuteBarData(WithCreateBarData, self.assertEqual(bar_data.can_trade(self.ASSET1), info[1]) -class TestMinuteBarDataMultipleExchanges(WithCreateBarData, - WithBarDataChecks, - ZiplineTestCase): +class TestMinuteBarDataFuturesCalendar(WithCreateBarData, + WithBarDataChecks, + ZiplineTestCase): START_DATE = pd.Timestamp('2016-01-05', tz='UTC') END_DATE = ASSET_FINDER_EQUITY_END_DATE = pd.Timestamp( @@ -742,20 +742,29 @@ class TestMinuteBarDataMultipleExchanges(WithCreateBarData, return pd.DataFrame.from_dict( { 6: { - 'symbol': 'CLG06', + 'symbol': 'CLH16', 'root_symbol': 'CL', - 'start_date': pd.Timestamp('2005-12-01', tz='UTC'), - 'notice_date': pd.Timestamp('2005-12-20', tz='UTC'), - 'expiration_date': pd.Timestamp('2006-01-20', tz='UTC'), + 'start_date': pd.Timestamp('2016-01-04', tz='UTC'), + 'notice_date': pd.Timestamp('2016-01-19', tz='UTC'), + 'expiration_date': pd.Timestamp('2016-02-19', tz='UTC'), 'exchange': 'ICEUS', }, + 7: { + 'symbol': 'FVH16', + 'root_symbol': 'FV', + 'start_date': pd.Timestamp('2016-01-04', tz='UTC'), + 'notice_date': pd.Timestamp('2016-01-22', tz='UTC'), + 'expiration_date': pd.Timestamp('2016-02-22', tz='UTC'), + 'auto_close_date': pd.Timestamp('2016-01-20', tz='UTC'), + 'exchange': 'CME', + }, }, orient='index', ) @classmethod def init_class_fixtures(cls): - super(TestMinuteBarDataMultipleExchanges, cls).init_class_fixtures() + super(TestMinuteBarDataFuturesCalendar, cls).init_class_fixtures() cls.trading_calendar = get_calendar('CME') def test_can_trade_multiple_exchange_closed(self): @@ -808,6 +817,30 @@ class TestMinuteBarDataMultipleExchanges(WithCreateBarData, self.assertEqual(info[1], series.loc[nyse_asset]) self.assertEqual(info[2], series.loc[ice_asset]) + def test_can_trade_delisted(self): + """ + Test that can_trade returns False for an asset on or after its auto + close date. + """ + auto_closing_asset = self.asset_finder.retrieve_asset(7) + + # Our asset's auto close date is 2016-01-20, which means that as of the + # market open for the 2016-01-20 session, `can_trade` should return + # False. + minutes_to_check = [ + (pd.Timestamp('2016-01-19 00:00:00', tz='UTC'), True), + (pd.Timestamp('2016-01-19 23:00:00', tz='UTC'), True), + (pd.Timestamp('2016-01-19 23:01:00', tz='UTC'), False), + (pd.Timestamp('2016-01-19 23:59:00', tz='UTC'), False), + (pd.Timestamp('2016-01-20 00:00:00', tz='UTC'), False), + (pd.Timestamp('2016-01-20 00:01:00', tz='UTC'), False), + (pd.Timestamp('2016-01-21 00:00:00', tz='UTC'), False), + ] + + for info in minutes_to_check: + bar_data = self.create_bardata(simulation_dt_func=lambda: info[0]) + self.assertEqual(bar_data.can_trade(auto_closing_asset), info[1]) + class TestDailyBarData(WithCreateBarData, WithBarDataChecks, diff --git a/zipline/_protocol.pyx b/zipline/_protocol.pyx index 995b2153..9c1d48f9 100644 --- a/zipline/_protocol.pyx +++ b/zipline/_protocol.pyx @@ -508,6 +508,9 @@ cdef class BarData: # asset isn't alive return False + if asset.auto_close_date and session_label >= asset.auto_close_date: + return False + if not self._daily_mode: # Find the next market minute for this calendar, and check if this # asset's exchange is open at that minute. diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 69962113..9ed90fe8 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -1348,10 +1348,10 @@ class TradingAlgorithm(object): if asset.auto_close_date: day = normalize_date(self.get_datetime()) - if asset.end_date < day < asset.auto_close_date: - # we are between the asset's end date and auto close date, - # so warn the user that they can't place an order for this - # asset, and return None. + if day > min(asset.end_date, asset.auto_close_date): + # If we are after the asset's end date or auto close date, warn + # the user that they can't place an order for this asset, and + # return None. log.warn("Cannot place order for {0}, as it has de-listed. " "Any existing positions for this asset will be " "liquidated on "