diff --git a/tests/risk/answer_key.py b/tests/risk/answer_key.py index 27b82f63..58160c7a 100644 --- a/tests/risk/answer_key.py +++ b/tests/risk/answer_key.py @@ -163,66 +163,80 @@ class AnswerKey(object): # Below matches the inconsistent capitalization in spreadsheet 'BENCHMARK_PERIOD_RETURNS': { - 'Monthly': DataIndex('s_p', 'P', 8, 19), - '3-Month': DataIndex('s_p', 'Q', 10, 19), - '6-month': DataIndex('s_p', 'R', 13, 19), - 'year': DataIndex('s_p', 'S', 19, 19), + 'Monthly': DataIndex('s_p', 'R', 8, 19), + '3-Month': DataIndex('s_p', 'S', 10, 19), + '6-month': DataIndex('s_p', 'T', 13, 19), + 'year': DataIndex('s_p', 'U', 19, 19), }, 'BENCHMARK_PERIOD_VOLATILITY': { - 'Monthly': DataIndex('s_p', 'T', 8, 19), - '3-Month': DataIndex('s_p', 'U', 10, 19), - '6-month': DataIndex('s_p', 'V', 13, 19), - 'year': DataIndex('s_p', 'W', 19, 19), + 'Monthly': DataIndex('s_p', 'V', 8, 19), + '3-Month': DataIndex('s_p', 'W', 10, 19), + '6-month': DataIndex('s_p', 'X', 13, 19), + 'year': DataIndex('s_p', 'Y', 19, 19), }, 'ALGORITHM_PERIOD_RETURNS': { - 'Monthly': DataIndex('Sim Period', 'V', 23, 34), - '3-Month': DataIndex('Sim Period', 'W', 25, 34), - '6-month': DataIndex('Sim Period', 'X', 28, 34), - 'year': DataIndex('Sim Period', 'Y', 34, 34), - }, - - 'ALGORITHM_PERIOD_VOLATILITY': { 'Monthly': DataIndex('Sim Period', 'Z', 23, 34), '3-Month': DataIndex('Sim Period', 'AA', 25, 34), '6-month': DataIndex('Sim Period', 'AB', 28, 34), 'year': DataIndex('Sim Period', 'AC', 34, 34), }, - 'ALGORITHM_PERIOD_SHARPE': { - 'Monthly': DataIndex('Sim Period', 'AD', 23, 34), - '3-Month': DataIndex('Sim Period', 'AE', 25, 34), - '6-month': DataIndex('Sim Period', 'AF', 28, 34), - 'year': DataIndex('Sim Period', 'AG', 34, 34), - }, - - 'ALGORITHM_PERIOD_BETA': { + 'ALGORITHM_PERIOD_VOLATILITY': { 'Monthly': DataIndex('Sim Period', 'AH', 23, 34), '3-Month': DataIndex('Sim Period', 'AI', 25, 34), '6-month': DataIndex('Sim Period', 'AJ', 28, 34), 'year': DataIndex('Sim Period', 'AK', 34, 34), }, - 'ALGORITHM_PERIOD_ALPHA': { + 'ALGORITHM_PERIOD_SHARPE': { 'Monthly': DataIndex('Sim Period', 'AL', 23, 34), '3-Month': DataIndex('Sim Period', 'AM', 25, 34), '6-month': DataIndex('Sim Period', 'AN', 28, 34), 'year': DataIndex('Sim Period', 'AO', 34, 34), }, + 'ALGORITHM_PERIOD_BETA': { + 'Monthly': DataIndex('Sim Period', 'AP', 23, 34), + '3-Month': DataIndex('Sim Period', 'AQ', 25, 34), + '6-month': DataIndex('Sim Period', 'AR', 28, 34), + 'year': DataIndex('Sim Period', 'AS', 34, 34), + }, + + 'ALGORITHM_PERIOD_ALPHA': { + 'Monthly': DataIndex('Sim Period', 'AT', 23, 34), + '3-Month': DataIndex('Sim Period', 'AU', 25, 34), + '6-month': DataIndex('Sim Period', 'AV', 28, 34), + 'year': DataIndex('Sim Period', 'AW', 34, 34), + }, + 'ALGORITHM_PERIOD_BENCHMARK_VARIANCE': { - 'Monthly': DataIndex('Sim Period', 'BB', 23, 34), - '3-Month': DataIndex('Sim Period', 'BC', 25, 34), - '6-month': DataIndex('Sim Period', 'BD', 28, 34), - 'year': DataIndex('Sim Period', 'BE', 34, 34), + 'Monthly': DataIndex('Sim Period', 'BJ', 23, 34), + '3-Month': DataIndex('Sim Period', 'BK', 25, 34), + '6-month': DataIndex('Sim Period', 'BL', 28, 34), + 'year': DataIndex('Sim Period', 'BM', 34, 34), }, 'ALGORITHM_PERIOD_COVARIANCE': { - 'Monthly': DataIndex('Sim Period', 'AX', 23, 34), - '3-Month': DataIndex('Sim Period', 'AY', 25, 34), - '6-month': DataIndex('Sim Period', 'AZ', 28, 34), - 'year': DataIndex('Sim Period', 'BA', 34, 34), + 'Monthly': DataIndex('Sim Period', 'BF', 23, 34), + '3-Month': DataIndex('Sim Period', 'BG', 25, 34), + '6-month': DataIndex('Sim Period', 'BH', 28, 34), + 'year': DataIndex('Sim Period', 'BI', 34, 34), + }, + + 'ALGORITHM_PERIOD_DOWNSIDE_RISK': { + 'Monthly': DataIndex('Sim Period', 'BN', 23, 34), + '3-Month': DataIndex('Sim Period', 'BO', 25, 34), + '6-month': DataIndex('Sim Period', 'BP', 28, 34), + 'year': DataIndex('Sim Period', 'BQ', 34, 34), + }, + + 'ALGORITHM_PERIOD_SORTINO': { + 'Monthly': DataIndex('Sim Period', 'BR', 23, 34), + '3-Month': DataIndex('Sim Period', 'BS', 25, 34), + '6-month': DataIndex('Sim Period', 'BT', 28, 34), + 'year': DataIndex('Sim Period', 'BU', 34, 34), }, 'ALGORITHM_RETURN_VALUES': DataIndex( @@ -241,16 +255,16 @@ class AnswerKey(object): 'Sim Cumulative', 'V', 4, 254), 'CUMULATIVE_INFORMATION': DataIndex( - 'Sim Cumulative', 'Y', 4, 254), + 'Sim Cumulative', 'AA', 4, 254), 'CUMULATIVE_BETA': DataIndex( - 'Sim Cumulative', 'AB', 4, 254), + 'Sim Cumulative', 'AD', 4, 254), 'CUMULATIVE_ALPHA': DataIndex( - 'Sim Cumulative', 'AC', 4, 254), + 'Sim Cumulative', 'AE', 4, 254), 'CUMULATIVE_MAX_DRAWDOWN': DataIndex( - 'Sim Cumulative', 'AF', 4, 254), + 'Sim Cumulative', 'AH', 4, 254), } diff --git a/tests/risk/risk-answer-key-checksums b/tests/risk/risk-answer-key-checksums index ccae28b7..3c8be657 100644 --- a/tests/risk/risk-answer-key-checksums +++ b/tests/risk/risk-answer-key-checksums @@ -13,3 +13,4 @@ cc507b6fca18aabadac69657181edd4e 651e611e723e2a58b1ded91d0cd39b66 d62fce39ec78f032165d8f356bba5c2c 97632f6f64dfc4a2de09882419a79421 +79d117cd4849745bf72ee1fd7442ef89 diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index a60561cc..cdfd03ef 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -71,15 +71,13 @@ class TestRisk(unittest.TestCase): np.testing.assert_almost_equal( self.cumulative_metrics_06.metrics.sharpe[dt], value, - 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, + self.cumulative_metrics_06.metrics.downside_risk[dt], err_msg="Mismatch at %s" % (dt,)) def test_sortino_06(self): @@ -87,15 +85,14 @@ class TestRisk(unittest.TestCase): np.testing.assert_almost_equal( self.cumulative_metrics_06.metrics.sortino[dt], value, - decimal=2, + decimal=4, 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, + self.cumulative_metrics_06.metrics.information[dt], err_msg="Mismatch at %s" % (dt,)) def test_alpha_06(self): @@ -103,15 +100,13 @@ class TestRisk(unittest.TestCase): 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, + self.cumulative_metrics_06.metrics.beta[dt], err_msg="Mismatch at %s" % (dt,)) def test_max_drawdown_06(self): @@ -119,5 +114,4 @@ class TestRisk(unittest.TestCase): np.testing.assert_almost_equal( self.cumulative_metrics_06.max_drawdowns[dt], value, - decimal=2, err_msg="Mismatch at %s" % (dt,)) diff --git a/tests/risk/test_risk_period.py b/tests/risk/test_risk_period.py index a86acad3..833f70d5 100644 --- a/tests/risk/test_risk_period.py +++ b/tests/risk/test_risk_period.py @@ -198,45 +198,44 @@ class TestRisk(unittest.TestCase): [x.sharpe for x in self.metrics_06.year_periods], ANSWER_KEY.ALGORITHM_PERIOD_SHARPE['year']) + def test_algorithm_downside_risk_06(self): + np.testing.assert_almost_equal( + [x.downside_risk for x in self.metrics_06.month_periods], + ANSWER_KEY.ALGORITHM_PERIOD_DOWNSIDE_RISK['Monthly'], + decimal=4) + np.testing.assert_almost_equal( + [x.downside_risk for x in self.metrics_06.three_month_periods], + ANSWER_KEY.ALGORITHM_PERIOD_DOWNSIDE_RISK['3-Month'], + decimal=4) + np.testing.assert_almost_equal( + [x.downside_risk for x in self.metrics_06.six_month_periods], + ANSWER_KEY.ALGORITHM_PERIOD_DOWNSIDE_RISK['6-month'], + decimal=4) + np.testing.assert_almost_equal( + [x.downside_risk for x in self.metrics_06.year_periods], + ANSWER_KEY.ALGORITHM_PERIOD_DOWNSIDE_RISK['year'], + decimal=4) + def test_algorithm_sortino_06(self): - self.assertEqual([round(x.sortino, 3) - for x in self.metrics_06.month_periods], - [4.491, - -2.842, - -2.052, - 3.898, - 7.023, - -8.532, - 3.079, - -0.354, - -1.125, - 3.009, - 3.277, - -3.122]) - self.assertEqual([round(x.sortino, 3) - for x in self.metrics_06.three_month_periods], - [-0.769, - -1.043, - 6.677, - -2.77, - -3.209, - -6.769, - 1.253, - 1.085, - 3.659, - 1.674]) - self.assertEqual([round(x.sortino, 3) - for x in self.metrics_06.six_month_periods], - [-2.728, - -3.258, - -1.84, - -1.366, - -1.845, - -3.415, - 2.238]) - self.assertEqual([round(x.sortino, 3) - for x in self.metrics_06.year_periods], - [-0.524]) + np.testing.assert_almost_equal( + [x.sortino for x in self.metrics_06.month_periods], + ANSWER_KEY.ALGORITHM_PERIOD_SORTINO['Monthly'], + decimal=3) + + np.testing.assert_almost_equal( + [x.sortino for x in self.metrics_06.three_month_periods], + ANSWER_KEY.ALGORITHM_PERIOD_SORTINO['3-Month'], + decimal=3) + + np.testing.assert_almost_equal( + [x.sortino for x in self.metrics_06.six_month_periods], + ANSWER_KEY.ALGORITHM_PERIOD_SORTINO['6-month'], + decimal=3) + + np.testing.assert_almost_equal( + [x.sortino for x in self.metrics_06.year_periods], + ANSWER_KEY.ALGORITHM_PERIOD_SORTINO['year'], + decimal=3) def test_algorithm_information_06(self): self.assertEqual([round(x.information, 3) diff --git a/tests/test_events_through_risk.py b/tests/test_events_through_risk.py index 5e141ca0..f854500e 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: -31.56903265, - third_date: -11.459888981, + second_date: -22.322677, + third_date: -9.353741 } for bar in gen: diff --git a/tests/test_tradesimulation.py b/tests/test_tradesimulation.py index 24dcf006..aa4f382e 100644 --- a/tests/test_tradesimulation.py +++ b/tests/test_tradesimulation.py @@ -22,6 +22,7 @@ class TestTradeSimulation(TestCase): def test_minutely_emissions_generate_performance_stats_for_last_day(self): params = factory.create_simulation_parameters(num_days=1) + params.data_frequency = 'minute' params.emission_rate = 'minute' algo = NoopAlgorithm() algo.run(source=[], sim_params=params) diff --git a/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index 08a39358..7ebb455c 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -31,6 +31,8 @@ from . risk import ( check_entry, choose_treasury, downside_risk, + sharpe_ratio, + sortino_ratio, ) log = logbook.Logger('Risk Cumulative') @@ -40,54 +42,6 @@ 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 @@ -181,6 +135,11 @@ class RiskMetricsCumulative(object): self.algorithm_returns_cont = pd.Series(index=cont_index) self.benchmark_returns_cont = pd.Series(index=cont_index) + self.mean_returns_cont = pd.Series(index=cont_index) + self.annualized_mean_returns_cont = pd.Series(index=cont_index) + self.mean_benchmark_returns_cont = pd.Series(index=cont_index) + self.annualized_mean_benchmark_returns_cont = pd.Series( + index=cont_index) # The returns at a given time are read and reset from the respective # returns container. @@ -189,7 +148,7 @@ class RiskMetricsCumulative(object): self.mean_returns = None self.annualized_mean_returns = None self.mean_benchmark_returns = None - self.annualized_benchmark_returns = None + self.annualized_mean_benchmark_returns = None self.algorithm_cumulative_returns = pd.Series(index=cont_index) self.benchmark_cumulative_returns = pd.Series(index=cont_index) @@ -235,46 +194,61 @@ class RiskMetricsCumulative(object): self.algorithm_returns_cont[dt] = algorithm_returns self.algorithm_returns = self.algorithm_returns_cont[:dt] + self.num_trading_days = len(self.algorithm_returns) + 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.algorithm_cumulative_returns[dt] = \ + self.calculate_cumulative_returns(self.algorithm_returns) - self.annualized_mean_returns = self.mean_returns * 252 + algo_cumulative_returns_to_date = \ + self.algorithm_cumulative_returns[:dt] + + self.mean_returns_cont[dt] = \ + algo_cumulative_returns_to_date[dt] / self.num_trading_days + + self.mean_returns = self.mean_returns_cont[:dt] + + self.annualized_mean_returns_cont[dt] = \ + self.mean_returns_cont[dt] * 252 + + self.annualized_mean_returns = self.annualized_mean_returns_cont[:dt] + + if self.create_first_day_stats: + if len(self.mean_returns) == 1: + self.mean_returns = pd.Series( + {'null return': 0.0}).append(self.mean_returns) + self.annualized_mean_returns = pd.Series( + {'null return': 0.0}).append(self.annualized_mean_returns) self.benchmark_returns_cont[dt] = benchmark_returns self.benchmark_returns = self.benchmark_returns_cont[:dt] - 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.algorithm_cumulative_returns[dt] = \ - self.calculate_cumulative_returns(self.algorithm_returns) self.benchmark_cumulative_returns[dt] = \ self.calculate_cumulative_returns(self.benchmark_returns) + benchmark_cumulative_returns_to_date = \ + self.benchmark_cumulative_returns[:dt] + + self.mean_benchmark_returns_cont[dt] = \ + benchmark_cumulative_returns_to_date[dt] / self.num_trading_days + + self.mean_benchmark_returns = self.mean_benchmark_returns_cont[:dt] + + self.annualized_mean_benchmark_returns_cont[dt] = \ + self.mean_benchmark_returns_cont[dt] * 252 + + self.annualized_mean_benchmark_returns = \ + self.annualized_mean_benchmark_returns_cont[:dt] + if not self.algorithm_returns.index.equals( self.benchmark_returns.index ): @@ -333,6 +307,16 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" 'null return') self.benchmark_returns.index = pd.to_datetime( self.benchmark_returns.index) + if 'null return' in self.mean_returns: + self.mean_returns = self.mean_returns.drop( + 'null return') + self.mean_returns.index = pd.to_datetime( + self.mean_returns.index) + if 'null return' in self.annualized_mean_returns: + self.annualized_mean_returns = \ + self.annualized_mean_returns.drop('null return') + self.annualized_mean_returns.index = pd.to_datetime( + self.mean_returns.index) def to_dict(self): """ @@ -435,7 +419,7 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" return information_ratio( self.metrics.algorithm_volatility[self.latest_dt], self.annualized_mean_returns[self.latest_dt], - self.annualized_benchmark_returns[self.latest_dt]) + self.annualized_mean_benchmark_returns[self.latest_dt]) def calculate_alpha(self, dt): """ @@ -443,11 +427,13 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" """ return alpha(self.annualized_mean_returns[self.latest_dt], self.treasury_period_return, - self.annualized_benchmark_returns[self.latest_dt], + self.annualized_mean_benchmark_returns[self.latest_dt], self.metrics.beta[dt]) def calculate_volatility(self, daily_returns): - return np.std(daily_returns) * math.sqrt(252) + if len(daily_returns) <= 1: + return 0.0 + return np.std(daily_returns, ddof=1) * math.sqrt(252) def calculate_downside_risk(self): return downside_risk(self.algorithm_returns, @@ -468,8 +454,8 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" if len(self.annualized_mean_returns) < 2: return 0.0 - returns_matrix = np.vstack([self.annualized_mean_returns, - self.annualized_benchmark_returns]) + returns_matrix = np.vstack([self.algorithm_returns, + self.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 08882672..b8127a31 100644 --- a/zipline/finance/risk/period.py +++ b/zipline/finance/risk/period.py @@ -30,6 +30,7 @@ from . import risk from . risk import ( alpha, check_entry, + downside_risk, information_ratio, sharpe_ratio, sortino_ratio, @@ -90,6 +91,17 @@ class RiskMetricsPeriod(object): raise Exception(message) self.num_trading_days = len(self.benchmark_returns) + self.trading_day_counts = pd.stats.moments.rolling_count( + self.algorithm_returns, self.num_trading_days) + self.mean_algorithm_returns = pd.Series( + index=self.algorithm_returns.index) + for dt, ret in self.algorithm_returns.iterkv(): + self.mean_algorithm_returns[dt] = ( + self.algorithm_returns[:dt].sum() + / + self.trading_day_counts[dt] + ) + self.benchmark_volatility = self.calculate_volatility( self.benchmark_returns) self.algorithm_volatility = self.calculate_volatility( @@ -195,15 +207,17 @@ class RiskMetricsPeriod(object): self.algorithm_period_returns, self.treasury_period_return) - 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, + mar = downside_risk(self.algorithm_returns, + self.mean_algorithm_returns, + self.num_trading_days) + # Hold on to downside risk for debugging purposes. + self.downside_risk = mar + return sortino_ratio(self.algorithm_period_returns, + self.treasury_period_return, mar) def calculate_information(self): diff --git a/zipline/finance/risk/risk.py b/zipline/finance/risk/risk.py index 5ecc362e..f1199148 100644 --- a/zipline/finance/risk/risk.py +++ b/zipline/finance/risk/risk.py @@ -1,5 +1,5 @@ # -# Copyright 2013 Quantopian, Inc. +# Copyright 2014 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -99,19 +99,21 @@ def sharpe_ratio(algorithm_volatility, algorithm_return, treasury_return): float. The Sharpe ratio. """ if zp_math.tolerant_equals(algorithm_volatility, 0): - return 0.0 + return np.nan return (algorithm_return - treasury_return) / algorithm_volatility def downside_risk(algorithm_returns, mean_returns, normalization_factor): - rets = algorithm_returns - mar = mean_returns + rets = algorithm_returns.round(8) + mar = mean_returns.round(8) downside_diff = (rets[rets < mar] - mar[rets < mar]) - return np.std(downside_diff) * math.sqrt(normalization_factor) + if len(downside_diff) <= 1: + return 0.0 + return np.std(downside_diff, ddof=1) * math.sqrt(normalization_factor) -def sortino_ratio(algorithm_returns, algorithm_period_return, mar): +def sortino_ratio(algorithm_period_return, treasury_period_return, mar): """ http://en.wikipedia.org/wiki/Sortino_ratio @@ -125,17 +127,10 @@ def sortino_ratio(algorithm_returns, algorithm_period_return, mar): Returns: float. The Sortino ratio. """ - if len(algorithm_returns) == 0: + if zp_math.tolerant_equals(mar, 0): return 0.0 - rets = algorithm_returns - downside = (rets[rets < mar] - mar) ** 2 - dr = np.sqrt(downside.sum() / len(rets)) - - if zp_math.tolerant_equals(dr, 0): - return 0.0 - - return (algorithm_period_return - mar) / dr + return (algorithm_period_return - treasury_period_return) / mar def information_ratio(algorithm_returns, benchmark_returns):