From a9d698018ac40e74b1445e10e432c3420acde83b Mon Sep 17 00:00:00 2001 From: Andrew Liang Date: Tue, 12 Jul 2016 16:10:31 -0400 Subject: [PATCH] MAINT: Refactor checking, calculation and processing of capital changes AlgorithmSimulator will no longer check for capital changes. Instead, TradingAlgorithm find and calculate the changes, and PerformanceTracker will apply the changes --- zipline/algorithm.py | 70 +++++++++++++++++++++++++- zipline/finance/performance/period.py | 2 - zipline/finance/performance/tracker.py | 17 +------ zipline/gens/tradesimulation.py | 39 ++++---------- 4 files changed, 80 insertions(+), 48 deletions(-) diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 663103a7..4c759678 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -408,9 +408,13 @@ class TradingAlgorithm(object): self.benchmark_sid = kwargs.pop('benchmark_sid', None) - # A dictionary of capital changes keyed by timestamp + # A dictionary of capital changes, keyed by timestamp, indicating the + # target/delta of the capital changes, along with values self.capital_changes = kwargs.pop('capital_changes', {}) + # A dictionary of the actual capital change deltas, keyed by timestamp + self.capital_change_deltas = {} + def init_engine(self, get_loader): """ Construct and store a PipelineEngine from loader. @@ -785,6 +789,70 @@ class TradingAlgorithm(object): return daily_stats + def calculate_capital_changes(self, dt, emission_rate, is_interday): + """ + If there is a capital change for a given dt, this means the the change + occurs before `handle_data` on the given dt. In the case of the + change being a target value, the change will be computed on the + portfolio value according to prices at the given dt + """ + try: + capital_change = self.capital_changes[dt] + except KeyError: + return + + if emission_rate == 'daily': + # If we are running daily emission, prices won't + # necessarily be synced at the end of every minute, and we + # need the up-to-date prices for capital change + # calculations. We want to sync the prices as of the + # last market minute, and this is okay from a data portal + # perspective as we have technically not "advanced" to the + # current dt yet. + self.perf_tracker.position_tracker.sync_last_sale_prices( + self.trading_calendar.previous_minute( + dt + ), + False, + self.data_portal + ) + + # Calculate performance before we sync prices price for the current dt + self.perf_tracker.cumulative_performance.calculate_performance() + self.perf_tracker.todays_performance.calculate_performance() + + if capital_change['type'] == 'target': + # Get an updated portfolio value as of this dt, but do it in a way + # so that the performance is not recalculated. This is done so + # that `process_capital_change` can find the performance values + # for the end of the subperiod, which is the previous dt + self.perf_tracker.position_tracker.sync_last_sale_prices( + dt, + self._in_before_trading_start, + self.data_portal + ) + portfolio_value = \ + self.perf_tracker.position_tracker.stats().net_value + \ + self.perf_tracker.cumulative_performance.ending_cash + + capital_change_amount = capital_change['value'] - portfolio_value + + log.info('Processing capital change to target %s at %s. Capital ' + 'change delta is %s' % (capital_change['value'], dt, + capital_change_amount)) + elif capital_change['type'] == 'delta': + capital_change_amount = capital_change['value'] + log.info('Processing capital change of delta %s at %s' + % (capital_change_amount, dt)) + else: + log.error("Capital change %s does not indicate a valid type " + "('target' or 'delta')" % capital_change) + return + + self.capital_change_deltas.update({dt: capital_change_amount}) + self.perf_tracker.process_capital_change(capital_change_amount, + is_interday) + @api_method def get_environment(self, field='platform'): """Query the execution environment. diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index ab63d4ae..2aeab406 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -242,8 +242,6 @@ class PerformancePeriod(object): del self._payout_last_sale_prices[asset] def subdivide_period(self, capital_change): - self.calculate_performance() - # Apply the capital change to the ending cash self.ending_cash += capital_change diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index 7e39a1da..017bfea5 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -238,22 +238,7 @@ class PerformanceTracker(object): return _dict - def process_capital_changes(self, capital_change, dt, is_interday): - if capital_change['type'] == 'target': - capital_change_amount = capital_change['value'] - \ - self.cumulative_performance.as_portfolio().portfolio_value - log.info('Processing capital change to target %s at %s. Capital ' - 'change delta is %s' % (capital_change['value'], dt, - capital_change_amount)) - elif capital_change['type'] == 'delta': - capital_change_amount = capital_change['delta'] - log.info('Processing capital change of delta %s at %s' - % (capital_change_amount, dt)) - else: - log.error("Capital change %s does not indicate a valid type " - "('target' or 'delta')" % capital_change) - return - + def process_capital_change(self, capital_change_amount, is_interday): self.cumulative_performance.subdivide_period(capital_change_amount) if is_interday: diff --git a/zipline/gens/tradesimulation.py b/zipline/gens/tradesimulation.py index 8c0736b0..e44e1196 100644 --- a/zipline/gens/tradesimulation.py +++ b/zipline/gens/tradesimulation.py @@ -95,13 +95,13 @@ class AlgorithmSimulator(object): Main generator work loop. """ algo = self.algo + emission_rate = algo.perf_tracker.emission_rate def every_bar(dt_to_use, current_data=self.current_data, handle_data=algo.event_manager.handle_data): # called every tick (minute or day). - if dt_to_use in algo.capital_changes: - process_minute_capital_changes(dt_to_use) + calculate_minute_capital_changes(dt_to_use) self.simulation_dt = dt_to_use algo.on_dt_changed(dt_to_use) @@ -149,11 +149,9 @@ class AlgorithmSimulator(object): perf_tracker = algo.perf_tracker - if midnight_dt in algo.capital_changes: - # process any capital changes that came overnight - change = algo.capital_changes[midnight_dt] - perf_tracker.process_capital_changes(change, dt, - is_interday=True) + # process any capital changes that came overnight + algo.calculate_capital_changes( + midnight_dt, emission_rate=emission_rate, is_interday=True) # Get the positions before updating the date so that prices are # fetched for trading close instead of midnight @@ -203,33 +201,16 @@ class AlgorithmSimulator(object): def execute_order_cancellation_policy(): algo.blotter.execute_cancel_policy(DAY_END) - def process_minute_capital_changes(dt): - # If we are running daily emission, prices won't - # necessarily be synced at the end of every minute, and we - # need the up-to-date prices for capital change - # calculations. We want to sync the prices as of the - # last market minute, and this is okay from a data portal - # perspective as we have technically not "advanced" to the - # current dt yet. - algo.perf_tracker.position_tracker.sync_last_sale_prices( - self.algo.trading_calendar.previous_minute(dt), - False, - self.data_portal - ) - + def calculate_minute_capital_changes(dt): # process any capital changes that came between the last # and current minutes - change = algo.capital_changes[dt] - algo.perf_tracker.process_capital_changes( - change, - dt, - is_interday=False - ) + algo.calculate_capital_changes( + dt, emission_rate=emission_rate, is_interday=False) else: def execute_order_cancellation_policy(): pass - def process_minute_capital_changes(dt): + def calculate_minute_capital_changes(dt): pass for dt, action in self.clock: @@ -239,7 +220,7 @@ class AlgorithmSimulator(object): once_a_day(dt) elif action == DAY_END: # End of the day. - if algo.perf_tracker.emission_rate == 'daily': + if emission_rate == 'daily': handle_benchmark(normalize_date(dt)) execute_order_cancellation_policy()