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))