From 433f97c38fa1e17a8b9bf4a9a0974a26d51bfd35 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Mon, 12 Aug 2013 16:02:40 -0400 Subject: [PATCH] ENH: Improve headline Sharpe risk calculations. This could perhaps be labelled BUG, as well. Change the Sharpe (and algorithm volatiilty) value used to compare algorithms/backtests so that it is annualized and uses daily returns. Previously, the Sharpe metric was using the same calculation style as the fixed size periods, i.e. 3 Month, 6 Month, etc., which can use the geometric mean when comparing against the risk free. Change the Sharpe calculation to use the arithmetic mean differenc against the risk free rate, using daily (non-compounded) values. Also, use annualized mean returns. --- tests/risk/answer_key.py | 2 +- tests/risk/risk-answer-key-checksums | 1 + tests/risk/test_risk_cumulative.py | 53 +++++++++++++++++++++++++--- tests/test_events_through_risk.py | 10 +++--- zipline/finance/risk/cumulative.py | 52 +++++++++++++++++++++++---- zipline/finance/risk/period.py | 7 +++- zipline/finance/risk/risk.py | 10 ++++-- 7 files changed, 113 insertions(+), 22 deletions(-) diff --git a/tests/risk/answer_key.py b/tests/risk/answer_key.py index f8400351..cf5531bf 100644 --- a/tests/risk/answer_key.py +++ b/tests/risk/answer_key.py @@ -227,7 +227,7 @@ 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) diff --git a/tests/risk/risk-answer-key-checksums b/tests/risk/risk-answer-key-checksums index 3adda698..1f5d97a9 100644 --- a/tests/risk/risk-answer-key-checksums +++ b/tests/risk/risk-answer-key-checksums @@ -6,3 +6,4 @@ 97dfb557c3501179504926e4079e6446 cc507b6fca18aabadac69657181edd4e 5b48e6a70181d73ecb7f07df5a3092e2 +3343940379161143630503413627a53a diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index 23ad5266..d52dea62 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -15,15 +15,58 @@ 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,)) 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/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index 1974756a..a13c8603 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -13,29 +13,60 @@ # 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, + choose_treasury, 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) + + class RiskMetricsCumulative(object): """ :Usage: @@ -102,6 +133,7 @@ class RiskMetricsCumulative(object): # returns container. self.algorithm_returns = None self.benchmark_returns = None + self.annualized_mean_returns = None self.compounded_log_returns = pd.Series(index=cont_index) self.algorithm_period_returns = pd.Series(index=cont_index) @@ -143,6 +175,12 @@ class RiskMetricsCumulative(object): self.algorithm_returns_cont[dt] = algorithm_returns self.algorithm_returns = self.algorithm_returns_cont.valid() + 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() @@ -306,8 +344,8 @@ 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): """ @@ -337,7 +375,7 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {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_beta(self): """ 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."