diff --git a/tests/risk/answer_key.py b/tests/risk/answer_key.py index cf5531bf..cdd81e45 100644 --- a/tests/risk/answer_key.py +++ b/tests/risk/answer_key.py @@ -230,7 +230,14 @@ class AnswerKey(object): '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), + } def __init__(self): @@ -289,4 +296,8 @@ 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)))}) diff --git a/tests/risk/risk-answer-key-checksums b/tests/risk/risk-answer-key-checksums index 4442df24..a43fec17 100644 --- a/tests/risk/risk-answer-key-checksums +++ b/tests/risk/risk-answer-key-checksums @@ -8,3 +8,6 @@ cc507b6fca18aabadac69657181edd4e 5b48e6a70181d73ecb7f07df5a3092e2 3343940379161143630503413627a53a 820235c4157a3c55474836438019ef2e +75c1b1441efbc2431215835a5079ccc6 +37e3ea4a1788f1aa6f3ee0986bc625ae +651e611e723e2a58b1ded91d0cd39b66 diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index d52dea62..cd28218b 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -70,3 +70,19 @@ class TestRisk(unittest.TestCase): 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,)) diff --git a/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index 3b06c96e..3957cf10 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -29,7 +29,6 @@ from . risk import ( check_entry, information_ratio, choose_treasury, - sortino_ratio, ) log = logbook.Logger('Risk Cumulative') @@ -67,6 +66,26 @@ def sharpe_ratio(algorithm_volatility, annualized_return, treasury_return): / 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 + + class RiskMetricsCumulative(object): """ :Usage: @@ -80,6 +99,7 @@ class RiskMetricsCumulative(object): 'sharpe', 'algorithm_volatility', 'benchmark_volatility', + 'downside_risk', 'sortino', 'information', ) @@ -137,6 +157,7 @@ class RiskMetricsCumulative(object): # returns container. self.algorithm_returns = None self.benchmark_returns = None + self.mean_returns = None self.annualized_mean_returns = None self.compounded_log_returns = pd.Series(index=cont_index) @@ -258,6 +279,7 @@ 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() @@ -383,16 +405,13 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" 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): """ @@ -413,6 +432,12 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" def calculate_volatility(self, daily_returns): 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): """