diff --git a/tests/risk/answer_key.py b/tests/risk/answer_key.py index f8400351..a1eb8ecf 100644 --- a/tests/risk/answer_key.py +++ b/tests/risk/answer_key.py @@ -227,10 +227,26 @@ class AnswerKey(object): 'Sim Cumulative', 'D', 4, 254), 'ALGORITHM_CUMULATIVE_VOLATILITY': DataIndex( - 'Sim Cumulative', 'O', 4, 254), + 'Sim Cumulative', 'P', 4, 254), 'ALGORITHM_CUMULATIVE_SHARPE': DataIndex( - 'Sim Cumulative', 'R', 4, 254) + 'Sim Cumulative', 'R', 4, 254), + + 'CUMULATIVE_DOWNSIDE_RISK': DataIndex( + 'Sim Cumulative', 'U', 4, 254), + + 'CUMULATIVE_SORTINO': DataIndex( + 'Sim Cumulative', 'V', 4, 254), + + 'CUMULATIVE_INFORMATION': DataIndex( + 'Sim Cumulative', 'Y', 4, 254), + + 'CUMULATIVE_BETA': DataIndex( + 'Sim Cumulative', 'AB', 4, 254), + + 'CUMULATIVE_ALPHA': DataIndex( + 'Sim Cumulative', 'AC', 4, 254), + } def __init__(self): @@ -289,4 +305,15 @@ RISK_CUMULATIVE = pd.DataFrame({ 'volatility': pd.Series(dict(zip( DATES, ANSWER_KEY.ALGORITHM_CUMULATIVE_VOLATILITY))), 'sharpe': pd.Series(dict(zip( - DATES, ANSWER_KEY.ALGORITHM_CUMULATIVE_SHARPE)))}) + DATES, ANSWER_KEY.ALGORITHM_CUMULATIVE_SHARPE))), + 'downside_risk': pd.Series(dict(zip( + DATES, ANSWER_KEY.CUMULATIVE_DOWNSIDE_RISK))), + 'sortino': pd.Series(dict(zip( + DATES, ANSWER_KEY.CUMULATIVE_SORTINO))), + 'information': pd.Series(dict(zip( + DATES, ANSWER_KEY.CUMULATIVE_INFORMATION))), + 'alpha': pd.Series(dict(zip( + DATES, ANSWER_KEY.CUMULATIVE_ALPHA))), + 'beta': pd.Series(dict(zip( + DATES, ANSWER_KEY.CUMULATIVE_BETA))), +}) diff --git a/tests/risk/risk-answer-key-checksums b/tests/risk/risk-answer-key-checksums index 3adda698..a43fec17 100644 --- a/tests/risk/risk-answer-key-checksums +++ b/tests/risk/risk-answer-key-checksums @@ -6,3 +6,8 @@ 97dfb557c3501179504926e4079e6446 cc507b6fca18aabadac69657181edd4e 5b48e6a70181d73ecb7f07df5a3092e2 +3343940379161143630503413627a53a +820235c4157a3c55474836438019ef2e +75c1b1441efbc2431215835a5079ccc6 +37e3ea4a1788f1aa6f3ee0986bc625ae +651e611e723e2a58b1ded91d0cd39b66 diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index 23ad5266..7dd3f44e 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -15,15 +15,98 @@ import unittest -from . answer_key import AnswerKey +import datetime +import numpy as np +import pytz +import zipline.finance.risk as risk +from zipline.utils import factory -ANSWER_KEY = AnswerKey() +from zipline.finance.trading import SimulationParameters + +import answer_key +ANSWER_KEY = answer_key.ANSWER_KEY class TestRisk(unittest.TestCase): def setUp(self): - pass + start_date = datetime.datetime( + year=2006, + month=1, + day=1, + hour=0, + minute=0, + tzinfo=pytz.utc) + end_date = datetime.datetime( + year=2006, month=12, day=29, tzinfo=pytz.utc) - def tearDown(self): - pass + self.sim_params = SimulationParameters( + period_start=start_date, + period_end=end_date + ) + + self.algo_returns_06 = factory.create_returns_from_list( + answer_key.ALGORITHM_RETURNS.values, + self.sim_params + ) + + self.cumulative_metrics_06 = risk.RiskMetricsCumulative( + self.sim_params) + + for dt, returns in answer_key.RETURNS_DATA.iterrows(): + self.cumulative_metrics_06.update(dt, + returns['Algorithm Returns'], + returns['Benchmark Returns']) + + def test_algorithm_volatility_06(self): + np.testing.assert_almost_equal( + ANSWER_KEY.ALGORITHM_CUMULATIVE_VOLATILITY, + self.cumulative_metrics_06.metrics.algorithm_volatility.values) + + def test_sharpe_06(self): + for dt, value in answer_key.RISK_CUMULATIVE.sharpe.iterkv(): + np.testing.assert_almost_equal( + value, + self.cumulative_metrics_06.metrics.sharpe[dt], + decimal=2, + err_msg="Mismatch at %s" % (dt,)) + + def test_downside_risk_06(self): + for dt, value in answer_key.RISK_CUMULATIVE.downside_risk.iterkv(): + np.testing.assert_almost_equal( + self.cumulative_metrics_06.metrics.downside_risk[dt], + value, + decimal=2, + err_msg="Mismatch at %s" % (dt,)) + + def test_sortino_06(self): + for dt, value in answer_key.RISK_CUMULATIVE.sortino.iterkv(): + np.testing.assert_almost_equal( + self.cumulative_metrics_06.metrics.sortino[dt], + value, + decimal=2, + err_msg="Mismatch at %s" % (dt,)) + + def test_information_06(self): + for dt, value in answer_key.RISK_CUMULATIVE.information.iterkv(): + np.testing.assert_almost_equal( + self.cumulative_metrics_06.metrics.information[dt], + value, + decimal=2, + err_msg="Mismatch at %s" % (dt,)) + + def test_alpha_06(self): + for dt, value in answer_key.RISK_CUMULATIVE.alpha.iterkv(): + np.testing.assert_almost_equal( + self.cumulative_metrics_06.metrics.alpha[dt], + value, + decimal=2, + err_msg="Mismatch at %s" % (dt,)) + + def test_beta_06(self): + for dt, value in answer_key.RISK_CUMULATIVE.beta.iterkv(): + np.testing.assert_almost_equal( + self.cumulative_metrics_06.metrics.beta[dt], + value, + decimal=2, + err_msg="Mismatch at %s" % (dt,)) diff --git a/tests/test_events_through_risk.py b/tests/test_events_through_risk.py index 891ee22b..6026c4ad 100644 --- a/tests/test_events_through_risk.py +++ b/tests/test_events_through_risk.py @@ -145,8 +145,8 @@ class TestEventsThroughRisk(unittest.TestCase): # at least be an early warning against changes. expected_sharpe = { first_date: np.nan, - second_date: -1.630920, - third_date: -1.016842, + second_date: -31.56903265, + third_date: -11.459888981, } for bar in gen: @@ -305,9 +305,9 @@ class TestEventsThroughRisk(unittest.TestCase): self.assertEqual(1, len(algo.portfolio.positions), "There should " "be one position after the first day.") - self.assertTrue( - np.isnan( - crm.metrics.algorithm_volatility[algo.datetime.date()]), + self.assertEquals( + 0, + crm.metrics.algorithm_volatility[algo.datetime.date()], "On the first day algorithm volatility does not exist.") second_msg = gen.next() diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 2a46bf83..ae59e1ed 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -1208,7 +1208,7 @@ class TestPerformanceTracker(unittest.TestCase): commission=0.50) benchmark_event_1 = Event({ 'dt': start_dt, - 'returns': 1.0, + 'returns': 0.01, 'type': DATASOURCE_TYPE.BENCHMARK }) @@ -1218,7 +1218,7 @@ class TestPerformanceTracker(unittest.TestCase): 'bar', 11.0, 20, start_dt + datetime.timedelta(minutes=1)) benchmark_event_2 = Event({ 'dt': start_dt + datetime.timedelta(minutes=1), - 'returns': 2.0, + 'returns': 0.02, 'type': DATASOURCE_TYPE.BENCHMARK }) @@ -1270,3 +1270,8 @@ class TestPerformanceTracker(unittest.TestCase): msg_1['minute_perf']['period_close']) self.assertEquals(foo_event_2.dt, msg_2['minute_perf']['period_close']) + + # Ensure that a Sharpe value for cumulative metrics is being + # created. + self.assertIsNotNone(msg_1['cumulative_risk_metrics']['sharpe']) + self.assertIsNotNone(msg_2['cumulative_risk_metrics']['sharpe']) diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index 58dc6de4..154ec88d 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -114,7 +114,8 @@ class PerformanceTracker(object): self.cumulative_risk_metrics = \ risk.RiskMetricsCumulative(self.sim_params, - returns_frequency='daily') + returns_frequency='daily', + create_first_day_stats=True) self.minute_performance = PerformancePeriod( # initial cash is your capital base. diff --git a/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index 1974756a..7a9376fc 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -13,29 +13,107 @@ # See the License for the specific language governing permissions and # limitations under the License. - +import functools import logbook import math import numpy as np import zipline.finance.trading as trading +import zipline.utils.math_utils as zp_math import pandas as pd from pandas.tseries.tools import normalize_date - from . risk import ( alpha, check_entry, choose_treasury, - information_ratio, - sharpe_ratio, - sortino_ratio, ) log = logbook.Logger('Risk Cumulative') +choose_treasury = functools.partial(choose_treasury, lambda *args: '10year', + compound=False) + + +def sharpe_ratio(algorithm_volatility, annualized_return, treasury_return): + """ + http://en.wikipedia.org/wiki/Sharpe_ratio + + Args: + algorithm_volatility (float): Algorithm volatility. + algorithm_return (float): Algorithm return percentage. + treasury_return (float): Treasury return percentage. + + Returns: + float. The Sharpe ratio. + """ + if zp_math.tolerant_equals(algorithm_volatility, 0): + return np.nan + + return ( + (annualized_return - treasury_return) + # The square of the annualization factor is in the volatility, + # because the volatility is also annualized, + # i.e. the sqrt(annual factor) is in the volatility's numerator. + # So to have the the correct annualization factor for the + # Sharpe value's numerator, which should be the sqrt(annual factor). + # The square of the sqrt of the annual factor, i.e. the annual factor + # itself, is needed in the numerator to factor out the division by + # its square root. + / algorithm_volatility) + + +def sortino_ratio(annualized_algorithm_return, treasury_return, downside_risk): + """ + http://en.wikipedia.org/wiki/Sortino_ratio + + Args: + algorithm_returns (np.array-like): + Returns from algorithm lifetime. + algorithm_period_return (float): + Algorithm return percentage from latest period. + mar (float): Minimum acceptable return. + + Returns: + float. The Sortino ratio. + """ + if np.isnan(downside_risk) or zp_math.tolerant_equals(downside_risk, 0): + return 0.0 + + return (annualized_algorithm_return - treasury_return) / downside_risk + + +def information_ratio(algo_volatility, algorithm_return, benchmark_return): + """ + http://en.wikipedia.org/wiki/Information_ratio + + Args: + algorithm_returns (np.array-like): + All returns during algorithm lifetime. + benchmark_returns (np.array-like): + All benchmark returns during algo lifetime. + + Returns: + float. Information ratio. + """ + if zp_math.tolerant_equals(algo_volatility, 0): + return np.nan + + return ( + (algorithm_return - benchmark_return) + # The square of the annualization factor is in the volatility, + # because the volatility is also annualized, + # i.e. the sqrt(annual factor) is in the volatility's numerator. + # So to have the the correct annualization factor for the + # Sharpe value's numerator, which should be the sqrt(annual factor). + # The square of the sqrt of the annual factor, i.e. the annual factor + # itself, is needed in the numerator to factor out the division by + # its square root. + / algo_volatility) + + class RiskMetricsCumulative(object): """ :Usage: @@ -49,11 +127,14 @@ class RiskMetricsCumulative(object): 'sharpe', 'algorithm_volatility', 'benchmark_volatility', + 'downside_risk', 'sortino', 'information', ) - def __init__(self, sim_params, returns_frequency=None): + def __init__(self, sim_params, + returns_frequency=None, + create_first_day_stats=False): """ - @returns_frequency allows for configuration of the whether the benchmark and algorithm returns are in units of minutes or days, @@ -83,6 +164,8 @@ class RiskMetricsCumulative(object): self.sim_params = sim_params + self.create_first_day_stats = create_first_day_stats + if returns_frequency is None: returns_frequency = self.sim_params.emission_rate @@ -102,6 +185,10 @@ class RiskMetricsCumulative(object): # returns container. self.algorithm_returns = None self.benchmark_returns = None + self.mean_returns = None + self.annualized_mean_returns = None + self.mean_benchmark_returns = None + self.annualized_benchmark_returns = None self.compounded_log_returns = pd.Series(index=cont_index) self.algorithm_period_returns = pd.Series(index=cont_index) @@ -143,9 +230,41 @@ class RiskMetricsCumulative(object): self.algorithm_returns_cont[dt] = algorithm_returns self.algorithm_returns = self.algorithm_returns_cont.valid() + if self.create_first_day_stats: + if len(self.algorithm_returns) == 1: + self.algorithm_returns = pd.Series( + {'null return': 0.0}).append( + self.algorithm_returns) + + self.mean_returns = pd.rolling_mean(self.algorithm_returns, + window=len(self.algorithm_returns), + min_periods=1) + + self.annualized_mean_returns = self.mean_returns * 252 + self.benchmark_returns_cont[dt] = benchmark_returns self.benchmark_returns = self.benchmark_returns_cont.valid() + self.mean_benchmark_returns = pd.rolling_mean( + self.benchmark_returns, + window=len(self.benchmark_returns), + min_periods=1) + + self.annualized_benchmark_returns = self.mean_benchmark_returns * 252 + + if self.create_first_day_stats: + if len(self.benchmark_returns) == 1: + self.benchmark_returns = pd.Series( + {'null return': 0.0}).append( + self.benchmark_returns) + + self.mean_benchmark_returns = pd.rolling_mean( + self.benchmark_returns, + window=len(self.benchmark_returns), + min_periods=1) + + self.annualized_benchmark_returns = self.mean_benchmark_returns * 252 + self.num_trading_days = len(self.algorithm_returns) self.update_compounded_log_returns() @@ -197,10 +316,24 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" self.metrics.beta[dt] = self.calculate_beta() self.metrics.alpha[dt] = self.calculate_alpha(dt) self.metrics.sharpe[dt] = self.calculate_sharpe() + self.metrics.downside_risk[dt] = self.calculate_downside_risk() self.metrics.sortino[dt] = self.calculate_sortino() self.metrics.information[dt] = self.calculate_information() self.max_drawdown = self.calculate_max_drawdown() + if self.create_first_day_stats: + # Remove placeholder 0 return + if 'null return' in self.algorithm_returns: + self.algorithm_returns = self.algorithm_returns.drop( + 'null return') + self.algorithm_returns.index = pd.to_datetime( + self.algorithm_returns.index) + if 'null return' in self.benchmark_returns: + self.benchmark_returns = self.benchmark_returns.drop( + 'null return') + self.benchmark_returns.index = pd.to_datetime( + self.benchmark_returns.index) + def to_dict(self): """ Creates a dictionary representing the state of the risk report. @@ -306,38 +439,43 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" http://en.wikipedia.org/wiki/Sharpe_ratio """ return sharpe_ratio(self.metrics.algorithm_volatility[self.latest_dt], - self.algorithm_period_returns[self.latest_dt], - self.treasury_period_return) + self.annualized_mean_returns[self.latest_dt], + self.daily_treasury[self.latest_dt.date()]) - def calculate_sortino(self, mar=None): + def calculate_sortino(self): """ http://en.wikipedia.org/wiki/Sortino_ratio """ - if mar is None: - mar = self.treasury_period_return - - return sortino_ratio(self.algorithm_returns, - self.algorithm_period_returns[self.latest_dt], - mar) + return sortino_ratio(self.annualized_mean_returns[self.latest_dt], + self.daily_treasury[self.latest_dt.date()], + self.metrics.downside_risk[self.latest_dt]) def calculate_information(self): """ http://en.wikipedia.org/wiki/Information_ratio """ - return information_ratio(self.algorithm_returns, - self.benchmark_returns) + return information_ratio( + self.metrics.algorithm_volatility[self.latest_dt], + self.annualized_mean_returns[self.latest_dt], + self.annualized_benchmark_returns[self.latest_dt]) def calculate_alpha(self, dt): """ http://en.wikipedia.org/wiki/Alpha_(investment) """ - return alpha(self.algorithm_period_returns[self.latest_dt], + return alpha(self.annualized_mean_returns[self.latest_dt], self.treasury_period_return, - self.benchmark_period_returns[self.latest_dt], + self.annualized_benchmark_returns[self.latest_dt], self.metrics.beta[dt]) def calculate_volatility(self, daily_returns): - return np.std(daily_returns, ddof=1) * math.sqrt(self.num_trading_days) + return np.std(daily_returns) * math.sqrt(252) + + def calculate_downside_risk(self): + rets = self.algorithm_returns + mar = self.mean_returns + downside_diff = (rets[rets < mar] - mar).valid() + return np.std(downside_diff) * math.sqrt(252) def calculate_beta(self): """ @@ -350,11 +488,11 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" """ # it doesn't make much sense to calculate beta for less than two days, # so return none. - if len(self.algorithm_returns) < 2: + if len(self.annualized_mean_returns) < 2: return 0.0 - returns_matrix = np.vstack([self.algorithm_returns, - self.benchmark_returns]) + returns_matrix = np.vstack([self.annualized_mean_returns, + self.annualized_benchmark_returns]) C = np.cov(returns_matrix, ddof=1) algorithm_covariance = C[0][1] benchmark_variance = C[1][1] diff --git a/zipline/finance/risk/period.py b/zipline/finance/risk/period.py index dfcb28bf..91a8d46c 100644 --- a/zipline/finance/risk/period.py +++ b/zipline/finance/risk/period.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools + import logbook import math import numpy as np @@ -22,10 +24,10 @@ import zipline.finance.trading as trading import pandas as pd +import risk from . risk import ( alpha, check_entry, - choose_treasury, information_ratio, sharpe_ratio, sortino_ratio, @@ -33,6 +35,9 @@ from . risk import ( log = logbook.Logger('Risk Period') +choose_treasury = functools.partial(risk.choose_treasury, + risk.select_treasury_duration) + class RiskMetricsPeriod(object): def __init__(self, start_date, end_date, returns, diff --git a/zipline/finance/risk/risk.py b/zipline/finance/risk/risk.py index c270fb8e..b7b4e20b 100644 --- a/zipline/finance/risk/risk.py +++ b/zipline/finance/risk/risk.py @@ -233,8 +233,9 @@ def select_treasury_duration(start_date, end_date): return treasury_duration -def choose_treasury(treasury_curves, start_date, end_date): - treasury_duration = select_treasury_duration(start_date, end_date) +def choose_treasury(select_treasury, treasury_curves, start_date, end_date, + compound=True): + treasury_duration = select_treasury(start_date, end_date) end_day = end_date.replace(hour=0, minute=0, second=0, microsecond=0) search_day = None @@ -274,7 +275,10 @@ treasury history range." if search_day: td = end_date - start_date - return rate * (td.days + 1) / 365 + if compound: + return rate * (td.days + 1) / 365 + else: + return rate message = "No rate for end date = {dt} and term = {term}. Check \ that date doesn't exceed treasury history range."