mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-07 08:14:03 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
97dfb557c3501179504926e4079e6446
|
||||
cc507b6fca18aabadac69657181edd4e
|
||||
5b48e6a70181d73ecb7f07df5a3092e2
|
||||
3343940379161143630503413627a53a
|
||||
|
||||
@@ -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,))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user