From e6c156c50b0073858869262f23d17a952db68658 Mon Sep 17 00:00:00 2001 From: fawce Date: Fri, 3 May 2013 23:39:07 -0400 Subject: [PATCH] ENH: Add intraday risk and performance for minute emission. Both risk and performance now calculate performance since inception (cumulative) and since the open. Both periods are updated intraday and both are reported. Batch risk for periods starting after the end of the treasury curve history now use most recent curve. --- zipline/finance/performance.py | 106 +++++++++++++++++++++++++------ zipline/finance/risk.py | 112 +++++++++++++++++++++++---------- zipline/finance/trading.py | 2 + 3 files changed, 166 insertions(+), 54 deletions(-) diff --git a/zipline/finance/performance.py b/zipline/finance/performance.py index adfb5d11..ce46f57a 100644 --- a/zipline/finance/performance.py +++ b/zipline/finance/performance.py @@ -162,19 +162,46 @@ class PerformanceTracker(object): self.total_days = self.sim_params.days_in_period self.capital_base = self.sim_params.capital_base self.emission_rate = sim_params.emission_rate - self.cumulative_risk_metrics = \ - risk.RiskMetricsIterative(self.sim_params) self.emission_rate = sim_params.emission_rate + self.perf_periods = [] + if self.emission_rate == 'daily': self.all_benchmark_returns = pd.Series( index=trading.environment.trading_days) + self.intraday_risk_metrics = None + self.cumulative_risk_metrics = \ + risk.RiskMetricsIterative(self.sim_params) + elif self.emission_rate == 'minute': self.all_benchmark_returns = pd.Series(index=pd.date_range( self.sim_params.first_open, self.sim_params.last_close, freq='Min')) + self.intraday_risk_metrics = \ + risk.RiskMetricsIterative(self.sim_params) - # this performance period will span the entire simulation. + self.cumulative_risk_metrics = \ + risk.RiskMetricsIterative(self.sim_params) + self.cumulative_risk_metrics.initialize_daily_indices() + + self.minute_performance = PerformancePeriod( + # initial cash is your capital base. + self.capital_base, + # the cumulative period will be calculated over the + # entire test. + self.period_start, + self.period_end, + # don't save the transactions for the cumulative + # period + keep_transactions=False, + keep_orders=False, + # don't serialize positions for cumualtive period + serialize_positions=False + ) + self.perf_periods.append(self.minute_performance) + + # this performance period will span the entire simulation from + # inception. self.cumulative_performance = PerformancePeriod( # initial cash is your capital base. self.capital_base, @@ -188,6 +215,7 @@ class PerformanceTracker(object): # don't serialize positions for cumualtive period serialize_positions=False ) + self.perf_periods.append(self.cumulative_performance) # this performance period will span just the current market day self.todays_performance = PerformancePeriod( @@ -200,6 +228,7 @@ class PerformanceTracker(object): keep_orders=True, serialize_positions=True ) + self.perf_periods.append(self.todays_performance) self.saved_dt = self.period_start self.returns = [] @@ -255,9 +284,11 @@ class PerformanceTracker(object): # Naming as intraday to make clear that these results are # being updated per minute _dict['intraday_risk_metrics'] = \ - self.cumulative_risk_metrics.to_dict() + self.intraday_risk_metrics.to_dict() _dict['intraday_perf'] = self.todays_performance.to_dict( self.saved_dt) + _dict['cumulative_risk_metrics'] = \ + self.cumulative_risk_metrics.to_dict() return _dict @@ -267,26 +298,24 @@ class PerformanceTracker(object): if event.type == zp.DATASOURCE_TYPE.TRADE: #update last sale - self.cumulative_performance.update_last_sale(event) - self.todays_performance.update_last_sale(event) + for perf_period in self.perf_periods: + perf_period.update_last_sale(event) elif event.type == zp.DATASOURCE_TYPE.TRANSACTION: # Trade simulation always follows a transaction with the # TRADE event that was used to simulate it, so we don't # check for end of day rollover messages here. self.txn_count += 1 - self.cumulative_performance.execute_transaction( - event - ) - self.todays_performance.execute_transaction(event) + for perf_period in self.perf_periods: + perf_period.execute_transaction(event) elif event.type == zp.DATASOURCE_TYPE.DIVIDEND: - self.cumulative_performance.add_dividend(event) - self.todays_performance.add_dividend(event) + for perf_period in self.perf_periods: + perf_period.add_dividend(event) elif event.type == zp.DATASOURCE_TYPE.ORDER: - self.cumulative_performance.record_order(event) - self.todays_performance.record_order(event) + for perf_period in self.perf_periods: + perf_period.record_order(event) elif event.type == zp.DATASOURCE_TYPE.CUSTOM: pass @@ -294,14 +323,51 @@ class PerformanceTracker(object): self.all_benchmark_returns[event.dt] = event.returns #calculate performance as of last trade - self.cumulative_performance.calculate_performance() - self.todays_performance.calculate_performance() + for perf_period in self.perf_periods: + perf_period.calculate_performance() def handle_minute_close(self, dt): - #update risk metrics for cumulative performance - self.cumulative_risk_metrics.update(dt, - self.todays_performance.returns, - self.all_benchmark_returns[dt]) + + todays_date = self.market_close.replace(hour=0, minute=0, second=0, + microsecond=0) + + minute_returns = self.minute_performance.returns + self.minute_performance.rollover() + algo_minute_returns = pd.Series({dt: minute_returns}) + bench_minute_returns = pd.Series({dt: self.all_benchmark_returns[dt]}) + # the intraday risk is calculated on top of minute performance + # returns for the bench and the algo + self.intraday_risk_metrics.update(dt, + algo_minute_returns, + bench_minute_returns) + # the intraday risk metrics compound the minutely returns of the + # benchmark. + bench_since_open = self.intraday_risk_metrics.benchmark_returns[-1] + benchmark_returns = pd.Series({dt: bench_since_open}) + + # if we've reached market close, check on dividends + if dt == self.market_close: + for perf_period in self.perf_periods: + perf_period.update_dividends(todays_date) + + algorithm_returns = pd.Series({dt: self.todays_performance.returns}) + + self.intraday_risk_metrics.update(dt, + algorithm_returns, + benchmark_returns) + + self.cumulative_risk_metrics.update(todays_date, + algorithm_returns, + benchmark_returns) + + # if this is the close, save the returns objects for cumulative + # risk calculations + if dt == self.market_close: + todays_return_obj = zp.DailyReturn( + todays_date, + self.todays_performance.returns + ) + self.returns.append(todays_return_obj) def handle_market_close(self): # add the return results from today to the list of DailyReturn objects. diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index b61385c2..048c7547 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -284,37 +284,56 @@ that date doesn't exceed treasury history range." class RiskMetricsBase(object): - def __init__(self, start_date, end_date, returns): + def __init__(self, start_date, end_date, returns, + benchmark_returns=None): treasury_curves = trading.environment.treasury_curves - mask = ((treasury_curves.index >= start_date) & - (treasury_curves.index <= end_date)) + if treasury_curves.index[-1] >= start_date: + mask = ((treasury_curves.index >= start_date) & + (treasury_curves.index <= end_date)) - self.treasury_curves = treasury_curves[mask] + self.treasury_curves = treasury_curves[mask] + else: + # our test is beyond the treasury curve history + # so we'll use the last available treasury curve + self.treasury_curves = treasury_curves[-1:] self.start_date = start_date self.end_date = end_date - self.algorithm_period_returns, self.algorithm_returns = \ - self.calculate_period_returns(returns) - benchmark_returns = [ - x for x in trading.environment.benchmark_returns - if x.date >= returns[0].date and x.date <= returns[-1].date - ] + if not benchmark_returns: + benchmark_returns = [ + x for x in trading.environment.benchmark_returns + if x.date >= returns[0].date and + x.date <= returns[-1].date + ] - self.benchmark_period_returns, self.benchmark_returns = \ - self.calculate_period_returns(benchmark_returns) + self.runonce = True + self.algorithm_returns = self.mask_returns_to_period(returns) + self.benchmark_returns = self.mask_returns_to_period(benchmark_returns) + self.calculate_metrics() - if(len(self.benchmark_returns) != len(self.algorithm_returns)): + def calculate_metrics(self): + + self.benchmark_period_returns = \ + self.calculate_period_returns(self.benchmark_returns) + + self.algorithm_period_returns = \ + self.calculate_period_returns(self.algorithm_returns) + + if not self.algorithm_returns.index.equals( + self.benchmark_returns.index + ): message = "Mismatch between benchmark_returns ({bm_count}) and \ algorithm_returns ({algo_count}) in range {start} : {end}" message = message.format( bm_count=len(self.benchmark_returns), algo_count=len(self.algorithm_returns), - start=start_date, - end=end_date + start=self.start_date, + end=self.end_date ) raise Exception(message) + self.runonce = False self.num_trading_days = len(self.benchmark_returns) self.benchmark_volatility = self.calculate_volatility( @@ -399,7 +418,7 @@ class RiskMetricsBase(object): return '\n'.join(statements) - def calculate_period_returns(self, daily_returns): + def mask_returns_to_period(self, daily_returns): returns = pd.Series([x.returns for x in daily_returns], index=[x.date for x in daily_returns]) @@ -410,9 +429,11 @@ class RiskMetricsBase(object): (returns.index <= self.end_date) & trade_day_mask) returns = returns[mask] - period_returns = (1. + returns).prod() - 1 + return returns - return period_returns, returns + def calculate_period_returns(self, returns): + period_returns = (1. + returns).prod() - 1 + return period_returns def calculate_volatility(self, daily_returns): return np.std(daily_returns, ddof=1) * math.sqrt(self.num_trading_days) @@ -536,21 +557,18 @@ class RiskMetricsIterative(RiskMetricsBase): (all_trading_days <= self.end_date)) self.trading_days = all_trading_days[mask] + if sim_params.period_end not in self.trading_days: + last_day = pd.tseries.index.DatetimeIndex( + [sim_params.period_end] + ) + self.trading_days = self.trading_days.append(last_day) self.sim_params = sim_params if sim_params.emission_rate == 'daily': - self.algorithm_returns_cont = pd.Series(index=self.trading_days) - self.benchmark_returns_cont = pd.Series(index=self.trading_days) - + self.initialize_daily_indices() elif sim_params.emission_rate == 'minute': - - self.algorithm_returns_cont = pd.Series(index=pd.date_range( - sim_params.first_open, sim_params.last_close, - freq="Min")) - self.benchmark_returns_cont = pd.Series(index=pd.date_range( - sim_params.first_open, sim_params.last_close, - freq="Min")) + self.initialize_minute_indices(sim_params) self.algorithm_returns = None self.benchmark_returns = None @@ -576,6 +594,19 @@ class RiskMetricsIterative(RiskMetricsBase): self.max_drawdown = 0 self.current_max = -np.inf self.excess_returns = [] + self.daily_treasury = {} + + def initialize_minute_indices(self, sim_params): + self.algorithm_returns_cont = pd.Series(index=pd.date_range( + sim_params.first_open, sim_params.last_close, + freq="Min")) + self.benchmark_returns_cont = pd.Series(index=pd.date_range( + sim_params.first_open, sim_params.last_close, + freq="Min")) + + def initialize_daily_indices(self): + self.algorithm_returns_cont = pd.Series(index=self.trading_days) + self.benchmark_returns_cont = pd.Series(index=self.trading_days) @property def last_return_date(self): @@ -597,7 +628,9 @@ class RiskMetricsIterative(RiskMetricsBase): self.benchmark_period_returns.append( self.calculate_period_returns(self.benchmark_returns)) - if(len(self.benchmark_returns) != len(self.algorithm_returns)): + if not self.algorithm_returns.index.equals( + self.benchmark_returns.index + ): message = "Mismatch between benchmark_returns ({bm_count}) and \ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" message = message.format( @@ -614,11 +647,22 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" self.calculate_volatility(self.benchmark_returns)) self.algorithm_volatility.append( self.calculate_volatility(self.algorithm_returns)) - self.treasury_period_return = choose_treasury( - self.treasury_curves, - self.start_date, - self.algorithm_returns.index[-1] - ) + + # caching the treasury rates for the live case is a + # big speedup, because it avoids searching the treasury + # curves on every minute. + treasury_end = self.algorithm_returns.index[-1].replace( + hour=0, minute=0) + if treasury_end not in self.daily_treasury: + treasury_period_return = choose_treasury( + self.treasury_curves, + self.start_date, + self.algorithm_returns.index[-1] + ) + self.daily_treasury[treasury_end] =\ + treasury_period_return + self.treasury_period_return = \ + self.daily_treasury[treasury_end] self.excess_returns.append( self.algorithm_period_returns[-1] - self.treasury_period_return) self.beta.append(self.calculate_beta()[0]) diff --git a/zipline/finance/trading.py b/zipline/finance/trading.py index c9dde661..63986fe7 100644 --- a/zipline/finance/trading.py +++ b/zipline/finance/trading.py @@ -88,6 +88,8 @@ class TradingEnvironment(object): load(self.bm_symbol) self.treasury_curves = pd.Series(treasury_curves_map) + if max_date: + self.treasury_curves = self.treasury_curves[:max_date] self._period_trading_days = None self._trading_days_series = None