From 962347318d7628fb159b9b35ec0c1f12a3277630 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Mon, 4 Jan 2016 15:46:03 -0500 Subject: [PATCH 1/3] MAINT: Futures cash adjustment on change and calc. In preparation for the incoming changes which no longer push every bar through the tradesimulation, remove the adjustment of the period's cash on every pricing change of a held futures asset. Instead hold the last sale price for each held future either: - At the end of each peformance period update the last sale prices of all held futures, so that the pnl for the next period uses values derived from the cash difference between the end of the two periods. - When a transaction is processed for the Future, so that the correct amount is applied to each cash adjustment. (i.e. the cash adjustment is reset on every change of amount of the Future being held, so that multiple size and prices do not need to be tracked for the same asset.) Also, remove now unused dict of payout calculation modifier, since new calculation reads the value directly off of the asset. Remove update_last_sale test, since the method no longer returns a cash value. --- tests/test_perf_tracking.py | 20 ------- zipline/finance/performance/period.py | 57 ++++++++++++++++++- .../finance/performance/position_tracker.py | 11 ---- zipline/finance/performance/tracker.py | 8 +-- 4 files changed, 58 insertions(+), 38 deletions(-) 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..7590ae6e 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -149,6 +149,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 +198,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,15 +225,34 @@ 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] + price = pos.last_sale_price + + payout = ( + (price - old_price) + * + asset.contract_multiplier + * + pos.amount + ) + 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 - total_at_end = self.ending_cash + self.ending_value + total_at_end = self.ending_cash + self.ending_value + payout self.pnl = total_at_end - total_at_start if total_at_start != 0: @@ -242,6 +279,22 @@ 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] + amount = self.position_tracker.positions[asset].amount + price = txn.price + cash_adj = (price - old_price) * asset.contract_multiplier * \ + amount + 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 +495,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): From 7a6c6695f742b41a4b640d2c4e823ccbbcebc04f Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Tue, 5 Jan 2016 10:39:41 -0500 Subject: [PATCH 2/3] MAINT: Factor out payout calculation. --- zipline/finance/performance/period.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index 7590ae6e..c7a0be70 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__( @@ -229,15 +233,12 @@ class PerformancePeriod(object): payouts = [] for asset, old_price in iteritems(self._payout_last_sale_prices): pos = positions[asset] - price = pos.last_sale_price - - payout = ( - (price - old_price) - * - asset.contract_multiplier - * - pos.amount - ) + amount = pos.amount + payout = calc_payout( + asset.contract_multiplier, + amount, + old_price, + pos.last_sale_price) payouts.append(payout) return sum(payouts) @@ -283,10 +284,11 @@ class PerformancePeriod(object): if isinstance(asset, Future): try: old_price = self._payout_last_sale_prices[asset] - amount = self.position_tracker.positions[asset].amount + pos = self.position_tracker.positions[asset] + amount = pos.amount price = txn.price - cash_adj = (price - old_price) * asset.contract_multiplier * \ - amount + 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] From 1362f155c65489158644d5f8fe86715ac0295552 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Wed, 6 Jan 2016 10:17:37 -0500 Subject: [PATCH 3/3] BUG: Make payout affect ending_cash. The payout should be reflected in ending cash, not just the total used for pnl. --- zipline/finance/performance/period.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index c7a0be70..52df7522 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -252,8 +252,8 @@ class PerformancePeriod(object): 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 - total_at_end = self.ending_cash + self.ending_value + payout + 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 if total_at_start != 0: