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.
This commit is contained in:
Eddie Hebert
2013-08-12 16:02:40 -04:00
parent 822e21fa84
commit 433f97c38f
7 changed files with 113 additions and 22 deletions
+1 -1
View File
@@ -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)
+1
View File
@@ -6,3 +6,4 @@
97dfb557c3501179504926e4079e6446
cc507b6fca18aabadac69657181edd4e
5b48e6a70181d73ecb7f07df5a3092e2
3343940379161143630503413627a53a
+48 -5
View File
@@ -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,))
+5 -5
View File
@@ -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()
+45 -7
View File
@@ -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):
"""
+6 -1
View File
@@ -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,
+7 -3
View File
@@ -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."