diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 976d247c..dfe77f2b 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -2227,26 +2227,6 @@ class TestPositionTracker(unittest.TestCase): self.assertEquals(val, 0) self.assertNotIsInstance(val, (bool, np.bool_)) - def test_update_last_sale(self): - pt = perf.PositionTracker(self.env.asset_finder) - dt = pd.Timestamp("1984/03/06 3:00PM") - pos1 = perf.Position(1, amount=np.float64(100.0), - last_sale_date=dt, last_sale_price=10) - pos3 = perf.Position(3, amount=np.float64(100.0), - last_sale_date=dt, last_sale_price=10) - pt.update_positions({1: pos1, 3: pos3}) - - event1 = Event({'sid': 1, - 'price': 11, - 'dt': dt}) - event3 = Event({'sid': 3, - 'price': 11, - 'dt': dt}) - - # Check cash-adjustment return value - self.assertEqual(0, pt.update_last_sale(event1)) - self.assertEqual(100000, pt.update_last_sale(event3)) - def test_position_values_and_exposures(self): pt = perf.PositionTracker(self.env.asset_finder) dt = pd.Timestamp("1984/03/06 3:00PM") diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index 7f98ca94..52df7522 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -126,6 +126,10 @@ def calc_period_stats(pos_stats, ending_cash): net_leverage=net_leverage) +def calc_payout(contract_multiplier, amount, old_price, price): + return (price - old_price) * contract_multiplier * amount + + class PerformancePeriod(object): def __init__( @@ -149,6 +153,15 @@ class PerformancePeriod(object): self.pnl = 0.0 self.ending_cash = starting_cash + + # Keyed by asset, the previous last sale price of positions with + # payouts on price differences, e.g. Futures. + # + # This dt is not the previous minute to the minute for which the + # calculation is done, but the last sale price either before the period + # start, or when the price at execution. + self._payout_last_sale_prices = {} + # rollover initializes a number of self's attributes: self.rollover() self.keep_transactions = keep_transactions @@ -189,6 +202,15 @@ class PerformancePeriod(object): self.orders_by_modified = {} self.orders_by_id = OrderedDict() + payout_assets = self._payout_last_sale_prices.keys() + + for asset in payout_assets: + if asset in self._payout_last_sale_prices: + self._payout_last_sale_prices[asset] = \ + self.position_tracker.positions[asset].last_sale_price + else: + del self._payout_last_sale_prices[asset] + def handle_dividends_paid(self, net_cash_payment): if net_cash_payment: self.handle_cash_payment(net_cash_payment) @@ -207,14 +229,30 @@ class PerformancePeriod(object): def adjust_field(self, field, value): setattr(self, field, value) + def _get_payout_total(self, positions): + payouts = [] + for asset, old_price in iteritems(self._payout_last_sale_prices): + pos = positions[asset] + amount = pos.amount + payout = calc_payout( + asset.contract_multiplier, + amount, + old_price, + pos.last_sale_price) + payouts.append(payout) + + return sum(payouts) + def calculate_performance(self): pt = self.position_tracker pos_stats = pt.stats() self.ending_value = pos_stats.net_value self.ending_exposure = pos_stats.net_exposure + payout = self._get_payout_total(pt.positions) + total_at_start = self.starting_cash + self.starting_value - self.ending_cash = self.starting_cash + self.period_cash_flow + self.ending_cash = self.starting_cash + self.period_cash_flow + payout total_at_end = self.ending_cash + self.ending_value self.pnl = total_at_end - total_at_start @@ -242,6 +280,23 @@ class PerformancePeriod(object): def handle_execution(self, txn): self.period_cash_flow += self._calculate_execution_cash_flow(txn) + asset = self.asset_finder.retrieve_asset(txn.sid) + if isinstance(asset, Future): + try: + old_price = self._payout_last_sale_prices[asset] + pos = self.position_tracker.positions[asset] + amount = pos.amount + price = txn.price + cash_adj = calc_payout( + asset.contract_multiplier, amount, old_price, price) + self.adjust_cash(cash_adj) + if amount + txn.amount == 0: + del self._payout_last_sale_prices[asset] + else: + self._payout_last_sale_prices[asset] = price + except KeyError: + self._payout_last_sale_prices[asset] = txn.price + if self.keep_transactions: try: self.processed_transactions[txn.dt].append(txn) @@ -442,6 +497,8 @@ class PerformancePeriod(object): dict(self.orders_by_id) state_dict['orders_by_modified'] = \ dict(self.orders_by_modified) + state_dict['_payout_last_sale_prices'] = \ + self._payout_last_sale_prices STATE_VERSION = 3 state_dict[VERSION_LABEL] = STATE_VERSION diff --git a/zipline/finance/performance/position_tracker.py b/zipline/finance/performance/position_tracker.py index eac750be..9534d487 100644 --- a/zipline/finance/performance/position_tracker.py +++ b/zipline/finance/performance/position_tracker.py @@ -131,7 +131,6 @@ class PositionTracker(object): # Arrays for quick calculations of positions value self._position_value_multipliers = OrderedDict() self._position_exposure_multipliers = OrderedDict() - self._position_payout_multipliers = OrderedDict() self._unpaid_dividends = pd.DataFrame( columns=zp.DIVIDEND_PAYMENT_FIELDS, ) @@ -145,7 +144,6 @@ class PositionTracker(object): try: self._position_value_multipliers[sid] self._position_exposure_multipliers[sid] - self._position_payout_multipliers[sid] except KeyError: # Check if there is an AssetFinder if self.asset_finder is None: @@ -156,13 +154,10 @@ class PositionTracker(object): if isinstance(asset, Equity): self._position_value_multipliers[sid] = 1 self._position_exposure_multipliers[sid] = 1 - self._position_payout_multipliers[sid] = 0 if isinstance(asset, Future): self._position_value_multipliers[sid] = 0 self._position_exposure_multipliers[sid] = \ asset.contract_multiplier - self._position_payout_multipliers[sid] = \ - asset.contract_multiplier # Futures auto-close timing is controlled by the Future's # auto_close_date property self._insert_auto_close_position_date( @@ -234,14 +229,9 @@ class PositionTracker(object): return 0 pos = self.positions[sid] - old_price = pos.last_sale_price pos.last_sale_date = event.dt pos.last_sale_price = price - # Calculate cash adjustment on assets with multipliers - return ((price - old_price) * self._position_payout_multipliers[sid] - * pos.amount) - def update_positions(self, positions): # update positions in batch self.positions.update(positions) @@ -483,7 +473,6 @@ class PositionTracker(object): # Arrays for quick calculations of positions value self._position_value_multipliers = OrderedDict() self._position_exposure_multipliers = OrderedDict() - self._position_payout_multipliers = OrderedDict() # Update positions is called without a finder self.update_positions(state['positions']) diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index 6cfadb19..49a1dede 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -284,11 +284,7 @@ class PerformanceTracker(object): return _dict def _handle_event_price(self, event): - # updates last sale, and pays out a cash adjustment if applicable - cash_adjustment = self.position_tracker.update_last_sale(event) - if cash_adjustment != 0: - self.cumulative_performance.handle_cash_payment(cash_adjustment) - self.todays_performance.handle_cash_payment(cash_adjustment) + self.position_tracker.update_last_sale(event) def process_trade(self, event): self._handle_event_price(event) @@ -296,9 +292,9 @@ class PerformanceTracker(object): def process_transaction(self, event): self._handle_event_price(event) self.txn_count += 1 - self.position_tracker.execute_transaction(event) self.cumulative_performance.handle_execution(event) self.todays_performance.handle_execution(event) + self.position_tracker.execute_transaction(event) def process_dividend(self, dividend):