diff --git a/.travis.yml b/.travis.yml index 443db91e..775bb55e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: before_install: - sudo apt-get install gfortran install: - - cat etc/requirements_dev.txt | grep -v "^#" | grep -v "^$" | grep -v ipython | grep -v nose | xargs pip install + - cat etc/requirements_dev.txt | grep -v "^#" | grep -v "^$" | grep -v ipython | grep -v nose== | xargs pip install - etc/ordered_pip.sh etc/requirements.txt before_script: - "flake8 zipline tests" diff --git a/etc/requirements_dev.txt b/etc/requirements_dev.txt index 5ab3bd03..951cb229 100644 --- a/etc/requirements_dev.txt +++ b/etc/requirements_dev.txt @@ -4,6 +4,7 @@ pyzmq==2.2.0.1 # Testing nose==1.2.1 +nose-parameterized==0.1 nosexcover==1.0.7 coverage==3.5.3 diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 85f32175..60621727 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -14,6 +14,7 @@ # limitations under the License. import unittest +from nose_parameterized import parameterized import copy import random import datetime @@ -33,43 +34,48 @@ from zipline.finance.trading import TradingEnvironment class PerformanceTestCase(unittest.TestCase): def setUp(self): - self.benchmark_returns, self.treasury_curves = \ - factory.load_market_data() - - for n in range(100): - - random_index = random.randint( - 0, - len(self.treasury_curves) - ) - - self.dt = self.treasury_curves.keys()[random_index] - self.end_dt = self.dt + datetime.timedelta(days=365) - - now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) - - if self.end_dt <= now: - break - - assert self.end_dt <= now, """ -failed to find a date suitable daterange after 100 attempts. please double -check treasury and benchmark data in findb, and re-run the test.""" - - self.trading_environment = TradingEnvironment( - self.benchmark_returns, - self.treasury_curves, - period_start=self.dt, - period_end=self.end_dt - ) - self.onesec = datetime.timedelta(seconds=1) self.oneday = datetime.timedelta(days=1) self.tradingday = datetime.timedelta(hours=6, minutes=30) - self.dt = self.trading_environment.trading_day_map.keys()[random_index] + self.trading_environment, self.dt, self.end_dt = self.create_env() - def tearDown(self): - pass + def create_env(self, start_dt=None): + benchmark_returns, treasury_curves = \ + factory.load_market_data() + + if not start_dt: + for n in range(100): + + random_index = random.randint( + 0, + len(treasury_curves) + ) + + start_dt = treasury_curves.keys()[random_index] + end_dt = start_dt + datetime.timedelta(days=365) + + now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) + + if end_dt <= now: + break + else: + end_dt = start_dt + datetime.timedelta(days=365) + now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) + + assert end_dt <= now, """ +failed to find a date suitable daterange after 100 attempts. please double +check treasury and benchmark data in findb, and re-run the test.""" + assert start_dt < end_dt, "start_dt must be less than end_dt" + + trading_environment = TradingEnvironment( + benchmark_returns, + treasury_curves, + period_start=start_dt, + period_end=end_dt + ) + + return trading_environment, start_dt, end_dt def test_long_position(self): """ @@ -530,7 +536,18 @@ shares in position" "should be -400 for all trades and transactions in period" ) - def test_tracker(self): + @parameterized.expand([ + (datetime.datetime(year=2008, + month=10, + day=9, + tzinfo=pytz.utc),), + (datetime.datetime(year=2010, + month=10, + day=9, + tzinfo=pytz.utc),), + (None,), # random start_dt + ]) + def test_tracker(self, start_dt): trade_count = 100 sid = 133 @@ -538,12 +555,15 @@ shares in position" price_list = [price] * trade_count volume = [100] * trade_count trade_time_increment = datetime.timedelta(days=1) + + trading_environment, start_dt, end_dt = self.create_env(start_dt) + trade_history = factory.create_trade_history( sid, price_list, volume, trade_time_increment, - self.trading_environment, + trading_environment, source_id="factory1" ) @@ -555,27 +575,27 @@ shares in position" price2_list, volume, trade_time_increment, - self.trading_environment, + trading_environment, source_id="factory2" ) trade_history.extend(trade_history2) - self.trading_environment.period_start = trade_history[0].dt - self.trading_environment.period_end = trade_history[-1].dt - self.trading_environment.first_open = \ - self.trading_environment.calculate_first_open() - self.trading_environment.last_close = \ - self.trading_environment.calculate_last_close() - self.trading_environment.capital_base = 1000.0 - self.trading_environment.frame_index = [ + trading_environment.period_start = trade_history[0].dt + trading_environment.period_end = trade_history[-1].dt + trading_environment.first_open = \ + trading_environment.calculate_first_open() + trading_environment.last_close = \ + trading_environment.calculate_last_close() + trading_environment.capital_base = 1000.0 + trading_environment.frame_index = [ 'sid', 'volume', 'dt', 'price', 'changed'] perf_tracker = perf.PerformanceTracker( - self.trading_environment + trading_environment ) # date_sort requires 'DONE' messages from each source @@ -594,7 +614,8 @@ shares in position" events = itertools.chain(events, [ndict({'dt': 'DONE'})]) - events = [self.event_with_txn(event) for event in events] + events = [self.event_with_txn(event, trading_environment) + for event in events] list(perf_tracker.transform( itertools.groupby(events, attrgetter('dt')))) @@ -610,10 +631,10 @@ shares in position" self.assertEqual(perf_tracker.last_close, perf_tracker.cumulative_risk_metrics.end_date) - def event_with_txn(self, event): + def event_with_txn(self, event, env): #create a transaction for all but #first trade in each sid, to simulate None transaction - if event.dt != self.trading_environment.period_start \ + if event.dt != env.period_start \ and event.dt != 'DONE': txn = ndict({ 'sid': event.sid, diff --git a/tests/test_risk.py b/tests/test_risk.py index 42a18863..51c16060 100644 --- a/tests/test_risk.py +++ b/tests/test_risk.py @@ -627,12 +627,12 @@ class Risk(unittest.TestCase): 0.0038, 0.0044, 0.0043, - 0.0041]) + 0.004]) self.assertEqual([round(x.treasury_period_return, 4) for x in metrics.three_month_periods], [0.0114, - 0.0118, + 0.0116, 0.0122, 0.0125, 0.0129, @@ -640,7 +640,7 @@ class Risk(unittest.TestCase): 0.0123, 0.0128, 0.0125, - 0.0128]) + 0.0127]) self.assertEqual([round(x.treasury_period_return, 4) for x in metrics.six_month_periods], [0.0260, @@ -649,7 +649,7 @@ class Risk(unittest.TestCase): 0.0252, 0.0259, 0.0256, - 0.0258]) + 0.0257]) self.assertEqual([round(x.treasury_period_return, 4) for x in metrics.year_periods], diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index b7731665..9cf550b3 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -57,7 +57,6 @@ import datetime import math from collections import OrderedDict import bisect -from operator import itemgetter import numpy as np import numpy.linalg as la from zipline.utils.date_utils import epoch_now @@ -304,6 +303,12 @@ class RiskMetricsBase(object): return 1.0 - math.exp(max_drawdown) + @property + def treasury_durations(self): + return ['1month', '3month', '6month', + '1year', '2year', '3year', '5year', + '7year', '10year', '30year'] + def choose_treasury(self): td = self.end_date - self.start_date if td.days <= 31: @@ -327,37 +332,31 @@ class RiskMetricsBase(object): else: self.treasury_duration = '30year' - # in case end date is not a trading day, search for the next or - # previous market day for an interest rate. choose next in a tie. + end_day = self.end_date.replace(hour=0, minute=0, second=0) search_day = None - if self.end_date in self.treasury_curves: - search_day = self.end_date - else: + if end_day in self.treasury_curves: + rate = self.get_treasury_rate(end_day) + if rate is not None: + search_day = end_day + + if not search_day: + # in case end date is not a trading day or there is no treasury + # data, search for the previous day with an interest rate. search_days = self.treasury_curves.keys() - next_day = prev_day = None - # Find leftmost item greater than or equal to end_date - i = bisect.bisect_left(search_days, self.end_date) - if i != len(search_days): - next_day = search_days[i] - if i: - prev_day = search_days[i - 1] - - search_dist = None - if next_day and prev_day: - search_day, search_dist = \ - min(((dt, self.search_day_distance(dt)) - for dt in (next_day, prev_day)), key=itemgetter(1)) - else: - search_day = next_day or prev_day + # Find rightmost value less than or equal to end_day + i = bisect.bisect_right(search_days, end_day) + for prev_day in search_days[i - 1::-1]: + rate = self.get_treasury_rate(prev_day) + if rate is not None: + search_day = prev_day + search_dist = self.search_day_distance(prev_day) + break if search_day: - search_dist = search_dist or \ - self.search_day_distance(search_day) - if (search_dist is None or search_dist > 1) and \ - search_days[0] <= self.end_date <= search_days[-1]: + search_days[0] <= end_day <= search_days[-1]: message = "No rate within 1 trading day of end date = \ {dt} and term = {term}. Using {search_day}. Check that date doesn't exceed \ treasury history range." @@ -367,16 +366,8 @@ treasury history range." log.warn(message) if search_day: - curve = self.treasury_curves[search_day] - self.treasury_curve = curve - rate = self.treasury_curve[self.treasury_duration] - # 1month note data begins in 8/2001, - # so we can use 3month instead. - if rate is None and self.treasury_duration == '1month': - rate = self.treasury_curve['3month'] - - if rate is not None: - return rate * (td.days + 1) / 365 + self.treasury_curve = self.treasury_curves[search_day] + return rate * (td.days + 1) / 365 message = "No rate for end date = {dt} and term = {term}. Check \ that date doesn't exceed treasury history range." @@ -387,10 +378,25 @@ that date doesn't exceed treasury history range." raise Exception(message) def search_day_distance(self, dt): - tdd = self.trading_environment.trading_day_distance(self.end_date, dt) + tdd = self.trading_environment.trading_day_distance(dt, self.end_date) if tdd is None: return None - return tdd if tdd >= 0 else -1 * tdd + .5 # prev is 'farther' + assert tdd >= 0 + return tdd + + def get_treasury_rate(self, day): + rate = None + + curve = self.treasury_curves[day] + # 1month note data begins in 8/2001, + # so we can use 3month instead. + idx = self.treasury_durations.index(self.treasury_duration) + for duration in self.treasury_durations[idx:]: + rate = curve[duration] + if rate is not None: + break + + return rate class RiskMetricsIterative(RiskMetricsBase):