From 433f97c38fa1e17a8b9bf4a9a0974a26d51bfd35 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Mon, 12 Aug 2013 16:02:40 -0400 Subject: [PATCH 1/7] 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." From bfa94e9c91fae2316f587fb6c84a124dc93411c5 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Thu, 3 Oct 2013 11:36:50 -0400 Subject: [PATCH 2/7] ENH: Approximate stats for the first day of minute emission. Volatility needs mulitple values to calculate the stddev, so provide a day with zero returns to base the first day against. --- tests/test_perf_tracking.py | 5 ++++ zipline/finance/performance/tracker.py | 3 +- zipline/finance/risk/cumulative.py | 38 +++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 2a46bf83..6a62a184 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -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 a13c8603..3b06c96e 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -84,7 +84,9 @@ class RiskMetricsCumulative(object): '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, @@ -114,6 +116,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 @@ -175,6 +179,12 @@ 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) @@ -184,6 +194,19 @@ class RiskMetricsCumulative(object): self.benchmark_returns_cont[dt] = benchmark_returns self.benchmark_returns = self.benchmark_returns_cont.valid() + 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() @@ -239,6 +262,19 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" 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. From 1afc9069b11eeed049f9264a3e06581b84d7fc80 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Wed, 9 Oct 2013 15:18:30 -0400 Subject: [PATCH 3/7] TST: Update answer key with annualized sortino and information. Include in the Excel answer sheet the sortino and information metrics using annualized inputs. --- tests/risk/risk-answer-key-checksums | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/risk/risk-answer-key-checksums b/tests/risk/risk-answer-key-checksums index 1f5d97a9..4442df24 100644 --- a/tests/risk/risk-answer-key-checksums +++ b/tests/risk/risk-answer-key-checksums @@ -7,3 +7,4 @@ cc507b6fca18aabadac69657181edd4e 5b48e6a70181d73ecb7f07df5a3092e2 3343940379161143630503413627a53a +820235c4157a3c55474836438019ef2e From 0ebdb2fe771bbc8ddc631ea547e8eb839172ae3e Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Wed, 9 Oct 2013 16:11:50 -0400 Subject: [PATCH 4/7] ENH: Annualize sortino ratio. Use annualized values for sortino, so that it is calculated using the same units as sharpe, etc. --- tests/risk/answer_key.py | 15 ++++++++-- tests/risk/risk-answer-key-checksums | 3 ++ tests/risk/test_risk_cumulative.py | 16 +++++++++++ zipline/finance/risk/cumulative.py | 41 ++++++++++++++++++++++------ 4 files changed, 65 insertions(+), 10 deletions(-) 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): """ From dcae6af67bd49caaa61b74a73b992e487019498d Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Wed, 9 Oct 2013 16:58:33 -0400 Subject: [PATCH 5/7] ENH: Annualize information ratio. Use annualized values for information, so that it is calculated using the same units as sharpe, etc. --- tests/risk/answer_key.py | 7 ++++- tests/risk/test_risk_cumulative.py | 8 ++++++ zipline/finance/risk/cumulative.py | 45 ++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/tests/risk/answer_key.py b/tests/risk/answer_key.py index cdd81e45..4366257a 100644 --- a/tests/risk/answer_key.py +++ b/tests/risk/answer_key.py @@ -238,6 +238,9 @@ class AnswerKey(object): 'CUMULATIVE_SORTINO': DataIndex( 'Sim Cumulative', 'V', 4, 254), + 'CUMULATIVE_INFORMATION': DataIndex( + 'Sim Cumulative', 'Y', 4, 254), + } def __init__(self): @@ -300,4 +303,6 @@ RISK_CUMULATIVE = pd.DataFrame({ 'downside_risk': pd.Series(dict(zip( DATES, ANSWER_KEY.CUMULATIVE_DOWNSIDE_RISK))), 'sortino': pd.Series(dict(zip( - DATES, ANSWER_KEY.CUMULATIVE_SORTINO)))}) + DATES, ANSWER_KEY.CUMULATIVE_SORTINO))), + 'information': pd.Series(dict(zip( + DATES, ANSWER_KEY.CUMULATIVE_INFORMATION)))}) diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index cd28218b..6fcf96ab 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -86,3 +86,11 @@ class TestRisk(unittest.TestCase): 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,)) diff --git a/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index 3957cf10..be59898e 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -27,7 +27,6 @@ from pandas.tseries.tools import normalize_date from . risk import ( alpha, check_entry, - information_ratio, choose_treasury, ) @@ -86,6 +85,35 @@ def sortino_ratio(annualized_algorithm_return, treasury_return, downside_risk): 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: @@ -159,6 +187,8 @@ class RiskMetricsCumulative(object): 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) @@ -215,6 +245,13 @@ class RiskMetricsCumulative(object): 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( @@ -417,8 +454,10 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" """ 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): """ From dad34d2ddbd3fdb236d558c33dba071a78a34cd3 Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Thu, 10 Oct 2013 10:42:47 -0400 Subject: [PATCH 6/7] TST: Add annualized alpha and beta to answer key. Add a column that uses annualized mean returns as the inputs into alpha and beta. --- tests/risk/answer_key.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/risk/answer_key.py b/tests/risk/answer_key.py index 4366257a..a1eb8ecf 100644 --- a/tests/risk/answer_key.py +++ b/tests/risk/answer_key.py @@ -241,6 +241,12 @@ class AnswerKey(object): '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): @@ -305,4 +311,9 @@ RISK_CUMULATIVE = pd.DataFrame({ 'sortino': pd.Series(dict(zip( DATES, ANSWER_KEY.CUMULATIVE_SORTINO))), 'information': pd.Series(dict(zip( - DATES, ANSWER_KEY.CUMULATIVE_INFORMATION)))}) + 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))), +}) From 1bad2456752b3e68976509d09f16b84d0b1b03ba Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Thu, 10 Oct 2013 11:47:28 -0400 Subject: [PATCH 7/7] ENH: Use annualized returns for beta and alpha. So that the units match the other risk calculations, also use annualized returns for beat and alpha. Update answer key to match values calculated on the first day. Also, update performance tracker test so that the returns used are fractional instead of > 1, so that the annualized numbers are more in line with real world values. --- tests/risk/test_risk_cumulative.py | 16 ++++++++++++++++ tests/test_perf_tracking.py | 4 ++-- zipline/finance/risk/cumulative.py | 10 +++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/risk/test_risk_cumulative.py b/tests/risk/test_risk_cumulative.py index 6fcf96ab..7dd3f44e 100644 --- a/tests/risk/test_risk_cumulative.py +++ b/tests/risk/test_risk_cumulative.py @@ -94,3 +94,19 @@ class TestRisk(unittest.TestCase): 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_perf_tracking.py b/tests/test_perf_tracking.py index 6a62a184..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 }) diff --git a/zipline/finance/risk/cumulative.py b/zipline/finance/risk/cumulative.py index be59898e..7a9376fc 100644 --- a/zipline/finance/risk/cumulative.py +++ b/zipline/finance/risk/cumulative.py @@ -463,9 +463,9 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {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): @@ -488,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]