From 64ffa055c92f0206e342622c2b4e26f7b8d23930 Mon Sep 17 00:00:00 2001 From: Ryan Day Date: Mon, 28 Jan 2013 08:55:14 -0500 Subject: [PATCH 1/2] Add the Sortino ratio for downside risk --- tests/test_risk.py | 40 +++++++++++++++++++++++++++++++++ zipline/finance/risk.py | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/tests/test_risk.py b/tests/test_risk.py index 51c16060..d9e25f6d 100644 --- a/tests/test_risk.py +++ b/tests/test_risk.py @@ -327,6 +327,46 @@ class Risk(unittest.TestCase): for x in self.metrics_06.year_periods], [-0.066]) + 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]) + def dtest_algorithm_beta_06(self): self.assertEqual([round(x.beta, 3) for x in self.metrics_06.month_periods], diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index 63cb9f7e..49e5c2e5 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -144,6 +144,7 @@ class RiskMetricsBase(object): self.algorithm_returns) self.treasury_period_return = self.choose_treasury() self.sharpe = self.calculate_sharpe() + self.sortino = self.calculate_sortino() self.beta, self.algorithm_covariance, self.benchmark_variance, \ self.condition_number, self.eigen_values = self.calculate_beta() self.alpha = self.calculate_alpha() @@ -165,6 +166,7 @@ class RiskMetricsBase(object): 'algorithm_period_return': self.algorithm_period_returns, 'benchmark_period_return': self.benchmark_period_returns, 'sharpe': self.sharpe, + 'sortino': self.sortino, 'beta': self.beta, 'alpha': self.alpha, 'excess_return': self.excess_return, @@ -193,6 +195,7 @@ class RiskMetricsBase(object): "benchmark_volatility", "algorithm_volatility", "sharpe", + "sortino", "algorithm_covariance", "benchmark_variance", "beta", @@ -241,6 +244,27 @@ class RiskMetricsBase(object): return ((self.algorithm_period_returns - self.treasury_period_return) / self.algorithm_volatility) + def calculate_sortino(self, mar=None): + """ + http://en.wikipedia.org/wiki/Sortino_ratio + """ + if len(self.algorithm_returns) == 0: + return 0.0 + + if mar is None: + mar = self.treasury_period_return + + downside = [ + (x - mar)**2 + for x in self.algorithm_returns + if x < mar] + dr = math.sqrt(sum(downside) / len(self.algorithm_returns)) + + if dr == 0: + return 0.0 + + return ((self.algorithm_period_returns - mar) / dr) + def calculate_beta(self): """ @@ -425,6 +449,7 @@ class RiskMetricsIterative(RiskMetricsBase): self.algorithm_period_returns = [] self.benchmark_period_returns = [] self.sharpe = [] + self.sortino = [] self.beta = [] self.alpha = [] self.max_drawdown = 0 @@ -475,6 +500,7 @@ algorithm_returns ({algo_count}) in range {start} : {end}" self.beta.append(self.calculate_beta()[0]) self.alpha.append(self.calculate_alpha()) self.sharpe.append(self.calculate_sharpe()) + self.sortino.append(self.calculate_sortino()) self.max_drawdown = self.calculate_max_drawdown() def to_dict(self): @@ -491,6 +517,7 @@ algorithm_returns ({algo_count}) in range {start} : {end}" 'algorithm_period_return': self.algorithm_period_returns[-1], 'benchmark_period_return': self.benchmark_period_returns[-1], 'sharpe': self.sharpe[-1], + 'sortino': self.sortino[-1], 'beta': self.beta[-1], 'alpha': self.alpha[-1], 'excess_return': self.excess_returns[-1], @@ -520,6 +547,7 @@ algorithm_returns ({algo_count}) in range {start} : {end}" "benchmark_volatility", "algorithm_volatility", "sharpe", + "sortino", "algorithm_covariance", "benchmark_variance", "beta", @@ -598,6 +626,28 @@ algorithm_returns ({algo_count}) in range {start} : {end}" return (self.algorithm_period_returns[-1] - self.treasury_period_return) / self.algorithm_volatility[-1] + def calculate_sortino(self, mar=None): + """ + http://en.wikipedia.org/wiki/Sortino_ratio + """ + if len(self.algorithm_returns) == 0: + return 0.0 + + if mar is None: + mar = self.treasury_period_return + + downside = [ + (x - mar)**2 + for x in self.algorithm_returns + if x < mar] + dr = math.sqrt(sum(downside) / len(self.algorithm_returns)) + + if dr == 0: + return 0.0 + + return ((self.algorithm_period_returns[-1] - mar) / + dr) + def calculate_alpha(self): """ http://en.wikipedia.org/wiki/Alpha_(investment) From bb16eda1fab6fd1c8b7c61ed3d11a3ccdc9f8d64 Mon Sep 17 00:00:00 2001 From: Ryan Day Date: Mon, 28 Jan 2013 14:08:44 -0500 Subject: [PATCH 2/2] Force float value, and compare result against boundary --- zipline/finance/risk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index 49e5c2e5..968db9a1 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -258,9 +258,9 @@ class RiskMetricsBase(object): (x - mar)**2 for x in self.algorithm_returns if x < mar] - dr = math.sqrt(sum(downside) / len(self.algorithm_returns)) + dr = float(math.sqrt(sum(downside) / len(self.algorithm_returns))) - if dr == 0: + if dr < 0.000001: return 0.0 return ((self.algorithm_period_returns - mar) / dr) @@ -640,9 +640,9 @@ algorithm_returns ({algo_count}) in range {start} : {end}" (x - mar)**2 for x in self.algorithm_returns if x < mar] - dr = math.sqrt(sum(downside) / len(self.algorithm_returns)) + dr = float(math.sqrt(sum(downside) / len(self.algorithm_returns))) - if dr == 0: + if dr < 0.000001: return 0.0 return ((self.algorithm_period_returns[-1] - mar) /