diff --git a/tests/test_risk_compare_batch_iterative.py b/tests/test_risk_compare_batch_iterative.py index 4de3fab3..7ae27d86 100644 --- a/tests/test_risk_compare_batch_iterative.py +++ b/tests/test_risk_compare_batch_iterative.py @@ -88,12 +88,6 @@ class RiskCompareIterativeToBatch(unittest.TestCase): self.assertEqual( risk_metrics_original.end_date, risk_metrics_refactor.end_date) - self.assertEqual( - risk_metrics_original.treasury_duration, - risk_metrics_refactor.treasury_duration) - self.assertEqual( - risk_metrics_original.treasury_curve, - risk_metrics_refactor.treasury_curve) self.assertEqual( risk_metrics_original.treasury_period_return, risk_metrics_refactor.treasury_period_return) diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index 9e158e85..7059ffd6 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -58,7 +58,6 @@ Risk Report import logbook import datetime import math -import bisect import numpy as np import numpy.linalg as la from dateutil.relativedelta import relativedelta @@ -173,11 +172,120 @@ def alpha(algorithm_period_return, treasury_period_return, (treasury_period_return + beta * (benchmark_period_returns - treasury_period_return)) +########################### +# End Risk Metric Section # +########################### + + +def get_treasury_rate(treasury_curves, treasury_duration, day): + rate = None + + curve = treasury_curves[day] + # 1month note data begins in 8/2001, + # so we can use 3month instead. + idx = TREASURY_DURATIONS.index(treasury_duration) + for duration in TREASURY_DURATIONS[idx:]: + rate = curve[duration] + if rate is not None: + break + + return rate + + +def search_day_distance(end_date, dt): + tdd = trading.environment.trading_day_distance(dt, end_date) + if tdd is None: + return None + assert tdd >= 0 + return tdd + + +def select_treasury_duration(start_date, end_date): + td = end_date - start_date + if td.days <= 31: + treasury_duration = '1month' + elif td.days <= 93: + treasury_duration = '3month' + elif td.days <= 186: + treasury_duration = '6month' + elif td.days <= 366: + treasury_duration = '1year' + elif td.days <= 365 * 2 + 1: + treasury_duration = '2year' + elif td.days <= 365 * 3 + 1: + treasury_duration = '3year' + elif td.days <= 365 * 5 + 2: + treasury_duration = '5year' + elif td.days <= 365 * 7 + 2: + treasury_duration = '7year' + elif td.days <= 365 * 10 + 2: + treasury_duration = '10year' + else: + treasury_duration = '30year' + + return treasury_duration + + +def choose_treasury(treasury_curves, start_date, end_date): + treasury_duration = select_treasury_duration(start_date, end_date) + end_day = end_date.replace(hour=0, minute=0, second=0) + search_day = None + + if end_day in treasury_curves: + rate = get_treasury_rate(treasury_curves, + treasury_duration, + end_day) + if rate is not None: + search_day = end_day + + if not search_day: + # in case end date is not a trading day or there is no treasury + # data, search for the previous day with an interest rate. + search_days = treasury_curves.index + + # Find rightmost value less than or equal to end_day + i = search_days.searchsorted(end_day) + for prev_day in search_days[i - 1::-1]: + rate = get_treasury_rate(treasury_curves, + treasury_duration, + prev_day) + if rate is not None: + search_day = prev_day + search_dist = search_day_distance(end_date, prev_day) + break + + if search_day: + if (search_dist is None or search_dist > 1) and \ + search_days[0] <= end_day <= search_days[-1]: + message = "No rate within 1 trading day of end date = \ +{dt} and term = {term}. Using {search_day}. Check that date doesn't exceed \ +treasury history range." + message = message.format(dt=end_date, + term=treasury_duration, + search_day=search_day) + log.warn(message) + + if search_day: + td = end_date - start_date + return rate * (td.days + 1) / 365 + + message = "No rate for end date = {dt} and term = {term}. Check \ +that date doesn't exceed treasury history range." + message = message.format( + dt=end_date, + term=treasury_duration + ) + raise Exception(message) + class RiskMetricsBase(object): def __init__(self, start_date, end_date, returns): - self.treasury_curves = trading.environment.treasury_curves + treasury_curves = trading.environment.treasury_curves + mask = ((treasury_curves.index >= start_date) & + (treasury_curves.index <= end_date)) + + self.treasury_curves = treasury_curves[mask] self.start_date = start_date self.end_date = end_date @@ -208,7 +316,11 @@ class RiskMetricsBase(object): self.benchmark_returns) self.algorithm_volatility = self.calculate_volatility( self.algorithm_returns) - self.treasury_period_return = self.choose_treasury() + self.treasury_period_return = choose_treasury( + self.treasury_curves, + self.start_date, + self.end_date + ) self.sharpe = self.calculate_sharpe() self.sortino = self.calculate_sortino() self.information = self.calculate_information() @@ -396,95 +508,6 @@ class RiskMetricsBase(object): return 1.0 - math.exp(max_drawdown) - def choose_treasury(self): - td = self.end_date - self.start_date - if td.days <= 31: - self.treasury_duration = '1month' - elif td.days <= 93: - self.treasury_duration = '3month' - elif td.days <= 186: - self.treasury_duration = '6month' - elif td.days <= 366: - self.treasury_duration = '1year' - elif td.days <= 365 * 2 + 1: - self.treasury_duration = '2year' - elif td.days <= 365 * 3 + 1: - self.treasury_duration = '3year' - elif td.days <= 365 * 5 + 2: - self.treasury_duration = '5year' - elif td.days <= 365 * 7 + 2: - self.treasury_duration = '7year' - elif td.days <= 365 * 10 + 2: - self.treasury_duration = '10year' - else: - self.treasury_duration = '30year' - - end_day = self.end_date.replace(hour=0, minute=0, second=0) - search_day = None - - if end_day in self.treasury_curves: - rate = self.get_treasury_rate(end_day) - if rate is not None: - search_day = end_day - - if not search_day: - # in case end date is not a trading day or there is no treasury - # data, search for the previous day with an interest rate. - search_days = self.treasury_curves.keys() - - # Find rightmost value less than or equal to end_day - i = bisect.bisect_right(search_days, end_day) - for prev_day in search_days[i - 1::-1]: - rate = self.get_treasury_rate(prev_day) - if rate is not None: - search_day = prev_day - search_dist = self.search_day_distance(prev_day) - break - - if search_day: - if (search_dist is None or search_dist > 1) and \ - search_days[0] <= end_day <= search_days[-1]: - message = "No rate within 1 trading day of end date = \ -{dt} and term = {term}. Using {search_day}. Check that date doesn't exceed \ -treasury history range." - message = message.format(dt=self.end_date, - term=self.treasury_duration, - search_day=search_day) - log.warn(message) - - if search_day: - self.treasury_curve = self.treasury_curves[search_day] - return rate * (td.days + 1) / 365 - - message = "No rate for end date = {dt} and term = {term}. Check \ -that date doesn't exceed treasury history range." - message = message.format( - dt=self.end_date, - term=self.treasury_duration - ) - raise Exception(message) - - def search_day_distance(self, dt): - tdd = trading.environment.trading_day_distance(dt, self.end_date) - if tdd is None: - return None - assert tdd >= 0 - return tdd - - def get_treasury_rate(self, day): - rate = None - - curve = self.treasury_curves[day] - # 1month note data begins in 8/2001, - # so we can use 3month instead. - idx = TREASURY_DURATIONS.index(self.treasury_duration) - for duration in TREASURY_DURATIONS[idx:]: - rate = curve[duration] - if rate is not None: - break - - return rate - class RiskMetricsIterative(RiskMetricsBase): """Iterative version of RiskMetrics. @@ -556,7 +579,11 @@ algorithm_returns ({algo_count}) in range {start} : {end}" self.calculate_volatility(self.benchmark_returns)) self.algorithm_volatility.append( self.calculate_volatility(self.algorithm_returns)) - self.treasury_period_return = self.choose_treasury() + self.treasury_period_return = choose_treasury( + self.treasury_curves, + self.start_date, + self.end_date + ) self.excess_returns.append( self.algorithm_period_returns[-1] - self.treasury_period_return) self.beta.append(self.calculate_beta()[0])