From 817ed88e38abb4c3334bf142adc153567eeb20ef Mon Sep 17 00:00:00 2001 From: fawce Date: Mon, 4 Feb 2013 17:00:14 -0500 Subject: [PATCH] Adds dividends to performance tracking. Algorithm returns and the risk calculations that depend on them now include cash dividends. This commit does _not_ provide an API for user algorithms to access dividends. PerformanceTracker expects the dividend data to arrive as events, similar to the way that Trades arrive. Dividends are expected to have adjusted payment amounts that are inline with adjusted trades. PerformanceTracker maintains state of all the unpaid dividends in the position objects held in PerformancePeriod. Dividend objects contain all the relevant dates (declared, ex, payment) as well as net and gross amounts. Dividends are removed from the list as they are paid. Cash flow is not incremented until the payment day. This creates the possibility of a dividend being owed but not paid or realized before the end of a test. For example, a dividend with an ex_date of today may have a pay date 2 weeks in the future. Right now the algorithm does not receive any credit for unpaid dividends. Tests cover buying/selling around the ex_date and payment_date, and checking that the performance calculated is as expected. --- tests/test_perf_tracking.py | 433 ++++++++++++++++++++++++++++----- tests/test_transforms.py | 1 + zipline/finance/performance.py | 131 ++++++++-- zipline/gens/utils.py | 1 + zipline/utils/factory.py | 48 +++- 5 files changed, 530 insertions(+), 84 deletions(-) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 098e0abd..86996971 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -17,7 +17,6 @@ import collections import unittest from nose_parameterized import parameterized -import random import datetime import pytz import itertools @@ -28,55 +27,377 @@ import zipline.finance.performance as perf from zipline.utils.protocol_utils import ndict from zipline.gens.composites import date_sorted_sources - from zipline.finance.trading import TradingEnvironment +from zipline.utils.factory import create_random_trading_environment -class TestPerformance(unittest.TestCase): +onesec = datetime.timedelta(seconds=1) +oneday = datetime.timedelta(days=1) +tradingday = datetime.timedelta(hours=6, minutes=30) + + +class TestDividendPerformance(unittest.TestCase): def setUp(self): - self.onesec = datetime.timedelta(seconds=1) - self.oneday = datetime.timedelta(days=1) - self.tradingday = datetime.timedelta(hours=6, minutes=30) - self.trading_environment, self.dt, self.end_dt = self.create_env() + self.trading_environment, self.dt, self.end_dt = \ + create_random_trading_environment() - def create_env(self, start_dt=None): - benchmark_returns, treasury_curves = \ - factory.load_market_data() + self.trading_environment.capital_base = 10e3 - 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 + def test_long_position_receives_dividend(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.trading_environment ) - return trading_environment, start_dt, end_dt + dividend = factory.create_dividend( + 1, + 10.00, + events[0].dt, + events[1].dt, + events[2].dt + ) + + events.insert(1, dividend) + txn = factory.create_txn(1, 10.0, 100, self.dt+oneday) + events[2].TRANSACTION = txn + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(results[0]['daily_perf']['period_open'], events[0].dt) + self.assertEqual( + results[-1]['daily_perf']['period_open'], + events[-1].dt + ) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0.0, 0.0, 0.1, 0.1, 0.1]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0.0, 0.0, 0.10, 0.0, 0.0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, -1000, 1000, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [0, -1000, 0, 0, 0]) + + def test_post_ex_long_position_receives_no_dividend(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.trading_environment + ) + + dividend = factory.create_dividend( + 1, + 10.00, + events[0].dt, + events[1].dt, + events[2].dt + ) + + events.insert(1, dividend) + txn = factory.create_txn(1, 10.0, 100, events[3].dt) + events[3].TRANSACTION = txn + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0, 0, 0, 0, 0]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0, 0, 0, 0, 0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, 0, -1000, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [0, 0, -1000, -1000, -1000]) + + def test_selling_before_dividend_payment_still_gets_paid(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.trading_environment + ) + + dividend = factory.create_dividend( + 1, + 10.00, + events[0].dt, + events[1].dt, + events[3].dt + ) + + buy_txn = factory.create_txn(1, 10.0, 100, events[1].dt) + events[1].TRANSACTION = buy_txn + sell_txn = factory.create_txn(1, 10.0, -100, events[2].dt) + events[2].TRANSACTION = sell_txn + events.insert(1, dividend) + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0, 0, 0, 0.1, 0.1]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0, 0, 0, 0.1, 0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, -1000, 1000, 1000, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [0, -1000, 0, 1000, 1000]) + + def test_buy_and_sell_before_ex(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10, 10], + [100, 100, 100, 100, 100, 100], + oneday, + self.trading_environment + ) + + dividend = factory.create_dividend( + 1, + 10.00, + events[3].dt, + events[4].dt, + events[5].dt + ) + + buy_txn = factory.create_txn(1, 10.0, 100, events[1].dt) + events[1].TRANSACTION = buy_txn + sell_txn = factory.create_txn(1, 10.0, -100, events[2].dt) + events[2].TRANSACTION = sell_txn + events.insert(1, dividend) + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(len(results), 6) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0, 0, 0, 0, 0, 0]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0, 0, 0, 0, 0, 0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, -1000, 1000, 0, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [0, -1000, 0, 0, 0, 0]) + + def test_ending_before_pay_date(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.trading_environment + ) + + dividend = factory.create_dividend( + 1, + 10.00, + events[0].dt, + events[1].dt, + events[-1].dt + 10*oneday + ) + + buy_txn = factory.create_txn(1, 10.0, 100, events[1].dt) + events[1].TRANSACTION = buy_txn + events.insert(1, dividend) + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0, 0, 0, 0.0, 0.0]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0, 0, 0, 0, 0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, -1000, 0, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual( + cumulative_cash_flows, + [0, -1000, -1000, -1000, -1000] + ) + + def test_short_position_receives_no_dividend(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.trading_environment + ) + + dividend = factory.create_dividend( + 1, + 10.00, + events[0].dt, + events[1].dt, + events[2].dt + ) + + events.insert(1, dividend) + txn = factory.create_txn(1, 10.0, -100, self.dt+oneday) + events[2].TRANSACTION = txn + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, 1000, 0, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [0, 1000, 1000, 1000, 1000]) + + def test_no_position_receives_no_dividend(self): + #post some trades in the market + events = factory.create_trade_history( + 1, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.trading_environment + ) + + dividend = factory.create_dividend( + 1, + 10.00, + events[0].dt, + events[1].dt, + events[2].dt + ) + + events.insert(1, dividend) + perf_tracker = perf.PerformanceTracker(self.trading_environment) + transformed_events = list(perf_tracker.transform( + ((event.dt, [event]) for event in events)) + ) + + #flatten the list of events + results = [] + for te in transformed_events: + for event in te[1]: + for message in event.perf_messages: + results.append(message) + + perf_messages, risk = perf_tracker.handle_simulation_end() + results.append(perf_messages[0]) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0.0, 0.0, 0.0, 0.0, 0.0]) + daily_returns = [event['daily_perf']['returns'] for event in results] + self.assertEqual(daily_returns, [0.0, 0.0, 0.0, 0.0, 0.0]) + cash_flows = [event['daily_perf']['capital_used'] for event in results] + self.assertEqual(cash_flows, [0, 0, 0, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [0, 0, 0, 0, 0]) + + +class TestPositionPerformance(unittest.TestCase): + + def setUp(self): + self.trading_environment, self.dt, self.end_dt = \ + create_random_trading_environment() def test_long_position(self): """ @@ -88,11 +409,11 @@ check treasury and benchmark data in findb, and re-run the test.""" 1, [10, 10, 10, 11], [100, 100, 100, 100], - self.onesec, + onesec, self.trading_environment ) - txn = factory.create_txn(1, 10.0, 100, self.dt + self.onesec) + txn = factory.create_txn(1, 10.0, 100, self.dt + onesec) pp = perf.PerformancePeriod(1000.0) pp.execute_transaction(txn) @@ -102,7 +423,7 @@ check treasury and benchmark data in findb, and re-run the test.""" pp.calculate_performance() self.assertEqual( - pp.period_capital_used, + pp.period_cash_flow, -1 * txn.price * txn.amount, "capital used should be equal to the opposite of the transaction \ cost of sole txn in test" @@ -157,13 +478,13 @@ single short-sale transaction""" 1, [10, 10, 10, 11, 10, 9], [100, 100, 100, 100, 100, 100], - self.onesec, + onesec, self.trading_environment ) trades_1 = trades[:-2] - txn = factory.create_txn(1, 10.0, -100, self.dt + self.onesec) + txn = factory.create_txn(1, 10.0, -100, self.dt + onesec) pp = perf.PerformancePeriod(1000.0) pp.execute_transaction(txn) @@ -173,7 +494,7 @@ single short-sale transaction""" pp.calculate_performance() self.assertEqual( - pp.period_capital_used, + pp.period_cash_flow, -1 * txn.price * txn.amount, "capital used should be equal to the opposite of the transaction\ cost of sole txn in test" @@ -230,7 +551,7 @@ single short-sale transaction""" pp.calculate_performance() self.assertEqual( - pp.period_capital_used, + pp.period_cash_flow, 0, "capital used should be zero, there were no transactions in \ performance period" @@ -292,7 +613,7 @@ single short-sale transaction""" ppTotal.calculate_performance() self.assertEqual( - ppTotal.period_capital_used, + ppTotal.period_cash_flow, -1 * txn.price * txn.amount, "capital used should be equal to the opposite of the transaction \ cost of sole txn in test" @@ -347,7 +668,7 @@ trade after cover""" 1, [10, 10, 10, 11, 9, 8, 7, 8, 9, 10], [100, 100, 100, 100, 100, 100, 100, 100, 100, 100], - self.onesec, + onesec, self.trading_environment ) @@ -355,10 +676,10 @@ trade after cover""" 1, 10.0, -100, - self.dt + self.onesec + self.dt + onesec ) - cover_txn = factory.create_txn(1, 7.0, 100, self.dt + self.onesec * 6) + cover_txn = factory.create_txn(1, 7.0, 100, self.dt + onesec * 6) pp = perf.PerformancePeriod(1000.0) pp.execute_transaction(short_txn) @@ -373,7 +694,7 @@ trade after cover""" cover_txn_cost = cover_txn.price * cover_txn.amount self.assertEqual( - pp.period_capital_used, + pp.period_cash_flow, -1 * short_txn_cost - cover_txn_cost, "capital used should be equal to the net transaction costs" ) @@ -426,7 +747,7 @@ shares in position" 1, [10, 11, 11, 12], [100, 100, 100, 100], - self.onesec, + onesec, self.trading_environment ) @@ -434,7 +755,7 @@ shares in position" 1, [10, 11, 11, 12], [100, 100, 100, 100], - self.onesec, + onesec, self.trading_environment ) @@ -470,13 +791,13 @@ shares in position" 1, 10.0, -100, - self.dt + self.onesec * 4) + self.dt + onesec * 4) down_tick = factory.create_trade( 1, 10.0, 100, - trades[-1].dt + self.onesec) + trades[-1].dt + onesec) pp.rollover() diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 18fa69f3..c2f17051 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -302,6 +302,7 @@ class TestBatchTransform(TestCase): "First three iterations should return None." + "\n" + "i.e. no returned values until window is full'" + "%s" % (algo.history_return_price_decorator,)) + # After three Nones, the next value should be a data frame self.assertTrue(isinstance( algo.history_return_price_class[wl], diff --git a/zipline/finance/performance.py b/zipline/finance/performance.py index 5ed6cc16..d5e3a8cf 100644 --- a/zipline/finance/performance.py +++ b/zipline/finance/performance.py @@ -90,8 +90,9 @@ omitted). | ending_value | the total market value of the positions held at the | | | end of the period | +---------------+------------------------------------------------------+ - | capital_used | the net capital consumed (positive means spent) by | - | | buying and selling securities in the period | + | cash_flow | the cash flow in the period (negative means spent) | + | | from buying and selling securities in the period. | + | | Includes dividend payments in the period as well. | +---------------+------------------------------------------------------+ | starting_value| the total market value of the positions held at the | | | start of the period | @@ -212,13 +213,15 @@ class PerformanceTracker(object): new_snapshot = [] for event in snapshot: - event.perf_messages = self.process_event(event) - event.portfolio = self.get_portfolio() + messages = self.process_event(event) + if messages is not None: + event.perf_messages = messages + event.portfolio = self.get_portfolio() - del event['TRANSACTION'] - new_snapshot.append(event) + new_snapshot.append(event) - yield date, new_snapshot + if len(new_snapshot) > 0: + yield date, new_snapshot def get_portfolio(self): return self.cumulative_performance.as_portfolio() @@ -241,21 +244,31 @@ class PerformanceTracker(object): def process_event(self, event): - messages = [] - + messages = None self.event_count += 1 - while event.dt > self.market_close: - messages.append(self.handle_market_close()) + if event.type == zp.DATASOURCE_TYPE.TRADE: + messages = [] + while event.dt > self.market_close: + messages.append(self.handle_market_close()) - if event.TRANSACTION: - self.txn_count += 1 - self.cumulative_performance.execute_transaction(event.TRANSACTION) - self.todays_performance.execute_transaction(event.TRANSACTION) + if event.TRANSACTION: + self.txn_count += 1 + self.cumulative_performance.execute_transaction( + event.TRANSACTION + ) + self.todays_performance.execute_transaction(event.TRANSACTION) - #update last sale - self.cumulative_performance.update_last_sale(event) - self.todays_performance.update_last_sale(event) + #update last sale + self.cumulative_performance.update_last_sale(event) + self.todays_performance.update_last_sale(event) + del event['TRANSACTION'] + + elif event.type == zp.DATASOURCE_TYPE.DIVIDEND: + # TODO: confirm with @ehebert that positions objects + # are shared. (and that it is ok). + self.cumulative_performance.add_dividend(event) + self.todays_performance.add_dividend(event) #calculate performance as of last trade self.cumulative_performance.calculate_performance() @@ -267,6 +280,10 @@ class PerformanceTracker(object): # add the return results from today to the list of DailyReturn objects. todays_date = self.market_close.replace(hour=0, minute=0, second=0) + + self.cumulative_performance.update_dividends(todays_date) + self.todays_performance.update_dividends(todays_date) + todays_return_obj = risk.DailyReturn( todays_date, self.todays_performance.returns @@ -322,7 +339,6 @@ Last successful date: %s" % self.market_open) # not trigger an end of day, so we trigger the final # market close(s) here perf_messages = [] - while self.last_close > self.market_close: perf_messages.append(self.handle_market_close()) @@ -352,6 +368,33 @@ class Position(object): self.cost_basis = 0.0 # per share self.last_sale_price = 0.0 self.last_sale_date = 0.0 + self.dividends = [] + + def update_dividends(self, dt): + # TODO: should I have asserts for the dt to be at + # midnight? + payment = 0.0 + unpaid_dividends = [] + for dividend in self.dividends: + if dt == dividend.ex_date: + # if we own shares at midnight of the div_ex date + # we are entitled to the dividend. + dividend.amount_on_ex_date = self.amount + dividend.payment = self.amount * dividend.net_amount + + if dt == dividend.pay_date: + # if it is the payment date, include this + # dividend's actual payment (calculated on + # ex_date) + payment += dividend.payment + else: + unpaid_dividends.append(dividend) + + self.dividends = unpaid_dividends + return payment + + def add_dividend(self, dividend): + self.dividends.append(dividend) def update(self, txn): if(self.sid != txn.sid): @@ -408,7 +451,7 @@ class PerformancePeriod(object): self.period_close = period_close self.ending_value = 0.0 - self.period_capital_used = 0.0 + self.period_cash_flow = 0.0 self.pnl = 0.0 #sid => position object self.positions = positiondict() @@ -439,7 +482,7 @@ class PerformancePeriod(object): def rollover(self): self.starting_value = self.ending_value self.starting_cash = self.ending_cash - self.period_capital_used = 0.0 + self.period_cash_flow = 0.0 self.pnl = 0.0 self.processed_transactions = [] self.cumulative_capital_used = 0.0 @@ -457,11 +500,40 @@ class PerformancePeriod(object): self._position_last_sale_prices, [0]) return index + def add_dividend(self, div): + # The dividend is received on midnight of the dividend + # declared date. We calculate the dividends based on the amount of + # stock owned on midnight of the ex dividend date. However, the cash + # is not dispersed until the payment date, which is + # included in the event. + self.positions[div.sid].add_dividend(div) + + def update_dividends(self, todays_date): + """ + Check the payment date and ex date against today's date + to detrmine if we are owed a dividend payment or if the + payment has been disbursed. + """ + cash_payments = 0.0 + for sid, pos in self.positions.iteritems(): + cash_payments += pos.update_dividends(todays_date) + + if cash_payments > 0.0: + # credit our cash balance with the dividend payments + self.period_cash_flow += cash_payments + # debit our cumulative cash spent with the dividend + # payments + self.cumulative_capital_used -= cash_payments + + # recalculate performance, including the dividend + # paymtents + self.calculate_performance() + def calculate_performance(self): self.ending_value = self.calculate_positions_value() total_at_start = self.starting_cash + self.starting_value - self.ending_cash = self.starting_cash + self.period_capital_used + self.ending_cash = self.starting_cash + self.period_cash_flow total_at_end = self.ending_cash + self.ending_value self.pnl = total_at_end - total_at_start @@ -478,7 +550,7 @@ class PerformancePeriod(object): index = self.index_for_position(txn.sid) self._position_amounts[index] = position.amount - self.period_capital_used += -1 * txn.price * txn.amount + self.period_cash_flow += -1 * txn.price * txn.amount # Max Leverage # --------------- @@ -522,7 +594,9 @@ class PerformancePeriod(object): def __core_dict(self): rval = { 'ending_value': self.ending_value, - 'capital_used': self.period_capital_used, + # this field is renamed to capital_used for backward + # compatibility. + 'capital_used': self.period_cash_flow, 'starting_value': self.starting_value, 'starting_cash': self.starting_cash, 'ending_cash': self.ending_cash, @@ -567,7 +641,9 @@ class PerformancePeriod(object): # as_portfolio is called in an inner loop, # so repeated object creation becomes too expensive portfolio = self._portfolio_store - portfolio.capital_used = self.period_capital_used, + # maintaining the old name for the portfolio field for + # backward compatibility + portfolio.capital_used = self.period_cash_flow portfolio.starting_cash = self.starting_cash portfolio.portfolio_value = self.ending_cash + self.ending_value portfolio.pnl = self.pnl @@ -583,6 +659,7 @@ class PerformancePeriod(object): positions = self._positions_store for sid, pos in self.positions.iteritems(): + if sid not in positions: positions[sid] = zp.Position(sid) position = positions[sid] @@ -595,8 +672,8 @@ class PerformancePeriod(object): def get_positions_list(self): positions = [] for sid, pos in self.positions.iteritems(): - cur = pos.to_dict() - positions.append(cur) + if pos.amount != 0: + positions.append(pos.to_dict()) return positions diff --git a/zipline/gens/utils.py b/zipline/gens/utils.py index dd72cb41..d2f1b0fb 100644 --- a/zipline/gens/utils.py +++ b/zipline/gens/utils.py @@ -71,6 +71,7 @@ def create_trade(sid, price, amount, datetime, source_id="test_factory"): trade.low = price * .95 trade.high = price * 1.05 trade.volume = amount + trade.TRANSACTION = None return trade diff --git a/zipline/utils/factory.py b/zipline/utils/factory.py index fa5822de..83e5da33 100644 --- a/zipline/utils/factory.py +++ b/zipline/utils/factory.py @@ -30,7 +30,7 @@ from datetime import datetime, timedelta import zipline.finance.risk as risk from zipline.utils.date_utils import tuple_to_date -from zipline.protocol import Event +from zipline.protocol import Event, DATASOURCE_TYPE from zipline.sources import (SpecificEquityTrades, DataFrameSource, DataPanelSource) @@ -119,6 +119,38 @@ def create_trading_environment(year=2006, start=None, end=None, return trading_environment +def create_random_trading_environment(): + benchmark_returns, treasury_curves = load_market_data() + + for n in range(100): + + random_index = random.randint( + 0, + len(treasury_curves) + ) + + start_dt = treasury_curves.keys()[random_index] + end_dt = start_dt + timedelta(days=365) + + now = datetime.utcnow().replace(tzinfo=pytz.utc) + + if end_dt <= now: + break + + assert end_dt <= now, """ +failed to find a suitable daterange after 100 attempts. please double +check treasury and benchmark data in findb, and re-run the test.""" + + trading_environment = TradingEnvironment( + benchmark_returns, + treasury_curves, + period_start=start_dt, + period_end=end_dt + ) + + return trading_environment, start_dt, end_dt + + def get_next_trading_dt(current, interval, trading_calendar): next = current while True: @@ -143,6 +175,20 @@ def create_trade_history(sid, prices, amounts, interval, trading_calendar, return trades +def create_dividend(sid, payment, declared_date, ex_date, pay_date): + div = Event({ + 'sid': sid, + 'gross_amount': payment, + 'net_amount': payment, + 'dt': declared_date.replace(hour=0, minute=0, second=0), + 'ex_date': ex_date.replace(hour=0, minute=0, second=0), + 'pay_date': pay_date.replace(hour=0, minute=0, second=0), + 'type': DATASOURCE_TYPE.DIVIDEND + }) + + return div + + def create_txn(sid, price, amount, datetime): txn = Event({ 'sid': sid,