Merge branch with annualized cumulative risk metrics.

This commit is contained in:
Eddie Hebert
2013-10-11 00:27:20 -04:00
9 changed files with 311 additions and 43 deletions
+30 -3
View File
@@ -227,10 +227,26 @@ 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)
'Sim Cumulative', 'R', 4, 254),
'CUMULATIVE_DOWNSIDE_RISK': DataIndex(
'Sim Cumulative', 'U', 4, 254),
'CUMULATIVE_SORTINO': DataIndex(
'Sim Cumulative', 'V', 4, 254),
'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):
@@ -289,4 +305,15 @@ 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))),
'information': pd.Series(dict(zip(
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))),
})
+5
View File
@@ -6,3 +6,8 @@
97dfb557c3501179504926e4079e6446
cc507b6fca18aabadac69657181edd4e
5b48e6a70181d73ecb7f07df5a3092e2
3343940379161143630503413627a53a
820235c4157a3c55474836438019ef2e
75c1b1441efbc2431215835a5079ccc6
37e3ea4a1788f1aa6f3ee0986bc625ae
651e611e723e2a58b1ded91d0cd39b66
+88 -5
View File
@@ -15,15 +15,98 @@
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,))
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,))
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,))
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,))
+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()
+7 -2
View File
@@ -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
})
@@ -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'])
+2 -1
View File
@@ -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.
+161 -23
View File
@@ -13,29 +13,107 @@
# 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,
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)
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
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:
@@ -49,11 +127,14 @@ class RiskMetricsCumulative(object):
'sharpe',
'algorithm_volatility',
'benchmark_volatility',
'downside_risk',
'sortino',
'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,
@@ -83,6 +164,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
@@ -102,6 +185,10 @@ class RiskMetricsCumulative(object):
# returns container.
self.algorithm_returns = None
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)
@@ -143,9 +230,41 @@ 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)
self.annualized_mean_returns = self.mean_returns * 252
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(
{'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()
@@ -197,10 +316,24 @@ 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()
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.
@@ -306,38 +439,43 @@ 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):
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):
"""
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):
"""
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):
return np.std(daily_returns, ddof=1) * math.sqrt(self.num_trading_days)
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):
"""
@@ -350,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]
+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."