From 51e8b3244e710be325f51c101872abf671cc25b2 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Fri, 10 Jan 2014 14:25:19 -0500 Subject: [PATCH] ENH: Filter out empty positions from portfolio container. To help prevent algorithms from operating on positions that are not in the existing universe of stocks. Formerly, iterating over positions would return positions for stocks which had zero shares held. (Where an explicit check in algorithm code for `pos.amount != 0` could prevent from using a non-existent position.) --- tests/test_algorithm.py | 43 ++++++++++++++++++++++++++- zipline/finance/performance/period.py | 9 +++++- zipline/test_algorithms.py | 31 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 146ddcbc..1792c97a 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -27,7 +27,8 @@ from zipline.test_algorithms import (TestRegisterTransformAlgorithm, TestTargetAlgorithm, TestOrderPercentAlgorithm, TestTargetPercentAlgorithm, - TestTargetValueAlgorithm) + TestTargetValueAlgorithm, + EmptyPositionsAlgorithm) from zipline.sources import (SpecificEquityTrades, DataFrameSource, @@ -194,3 +195,43 @@ class TestTransformAlgorithm(TestCase): instant_fill=True) algo.run(self.df) + + +class TestPositions(TestCase): + + def setUp(self): + setup_logger(self) + self.sim_params = factory.create_simulation_parameters(num_days=4) + setup_logger(self) + + trade_history = factory.create_trade_history( + 1, + [10.0, 10.0, 11.0, 11.0], + [100, 100, 100, 300], + timedelta(days=1), + self.sim_params + ) + self.source = SpecificEquityTrades(event_list=trade_history) + + self.df_source, self.df = \ + factory.create_test_df_source(self.sim_params) + + self.panel_source, self.panel = \ + factory.create_test_panel_source(self.sim_params) + + def test_empty_portfolio(self): + algo = EmptyPositionsAlgorithm(sim_params=self.sim_params, + data_frequency='daily') + + daily_stats = algo.run(self.df) + + expected_position_count = [ + 0, # Before entering the first position + 1, # After entering, exiting on this date + 0, # After exiting + 0, + ] + + for i, expected in enumerate(expected_position_count): + self.assertEqual(daily_stats.ix[i]['num_positions'], + expected) diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index 0f3113d1..9ecf0db6 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -355,13 +355,20 @@ class PerformancePeriod(object): positions = self._positions_store for sid, pos in iteritems(self.positions): - if sid not in positions: + if pos.amount != 0 and sid not in positions: positions[sid] = zp.Position(sid) position = positions[sid] position.amount = pos.amount position.cost_basis = pos.cost_basis position.last_sale_price = pos.last_sale_price + # Remove positions with no amounts from portfolio container + # that is passed to the algorithm. + # These positions are still stored internally for use with + # dividends etc. + if pos.amount == 0 and sid in positions: + del positions[sid] + return positions def get_positions_list(self): diff --git a/zipline/test_algorithms.py b/zipline/test_algorithms.py index ab14f55b..30528510 100644 --- a/zipline/test_algorithms.py +++ b/zipline/test_algorithms.py @@ -633,3 +633,34 @@ class TALIBAlgorithm(TradingAlgorithm): else: result = (np.nan,) * len(t.talib_fn.output_names) self.talib_results[t].append(result) + + +class EmptyPositionsAlgorithm(TradingAlgorithm): + """ + An algorithm that ensures that 'phantom' positions do not appear + portfolio.positions in the case that a position has been entered + and fully exited. + """ + def initialize(self, *args, **kwargs): + self.ordered = False + self.exited = False + + def handle_data(self, data): + if not self.ordered: + for s in data: + self.order(s, 100) + self.ordered = True + + if not self.exited: + amounts = [pos.amount for pos + in self.portfolio.positions.itervalues()] + if ( + all([(amount == 100) for amount in amounts]) and + (len(amounts) == len(data.keys())) + ): + for stock in self.portfolio.positions: + self.order(stock, -100) + self.exited = True + + # Should be 0 when all positions are exited. + self.record(num_positions=len(self.portfolio.positions))