From f21bbe58fc6718f2fe5fb627e2cbf394a04f77ea Mon Sep 17 00:00:00 2001 From: Richard Frank Date: Fri, 28 Mar 2014 17:29:13 -0400 Subject: [PATCH] ENH: Allow for stock dividends, and in particular, Google's recent 2 for 1 stock split, where 1 class C share was distributed for each share of class A held. Now a dividend can specify a sid and ratio of stock that will be paid to owners of the original security. If the ratio is 2.0, then for every existing share, two shares will be paid. --- tests/test_perf_tracking.py | 50 +++++++++++++++++++++++++ zipline/finance/performance/period.py | 15 +++++++- zipline/finance/performance/position.py | 31 ++++++++++++--- zipline/utils/factory.py | 25 +++++++++++-- 4 files changed, 109 insertions(+), 12 deletions(-) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 31c37dff..3623bee9 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -348,6 +348,56 @@ class TestDividendPerformance(unittest.TestCase): [event['cumulative_perf']['ending_cash'] for event in results] self.assertEqual(cash_pos, [9000, 9000, 10000, 10000, 10000]) + def test_long_position_receives_stock_dividend(self): + with trading.TradingEnvironment(): + # post some trades in the market + events = [] + for sid in (1, 2): + events.extend( + factory.create_trade_history( + sid, + [10, 10, 10, 10, 10], + [100, 100, 100, 100, 100], + oneday, + self.sim_params) + ) + + dividend = factory.create_stock_dividend( + 1, + payment_sid=2, + ratio=2, + # declared date, when the algorithm finds out about + # the dividend + declared_date=events[1].dt, + # ex_date, when the algorithm is credited with the + # dividend + ex_date=events[1].dt, + # pay date, when the algorithm receives the dividend. + pay_date=events[2].dt + ) + + txn = create_txn(events[0], 10.0, 100) + events.insert(0, txn) + events.insert(1, dividend) + results = calculate_results(self, events) + + self.assertEqual(len(results), 5) + cumulative_returns = \ + [event['cumulative_perf']['returns'] for event in results] + self.assertEqual(cumulative_returns, [0.0, 0.0, 0.2, 0.2, 0.2]) + daily_returns = [event['daily_perf']['returns'] + for event in results] + self.assertEqual(daily_returns, [0.0, 0.0, 0.2, 0.0, 0.0]) + cash_flows = [event['daily_perf']['capital_used'] + for event in results] + self.assertEqual(cash_flows, [-1000, 0, 0, 0, 0]) + cumulative_cash_flows = \ + [event['cumulative_perf']['capital_used'] for event in results] + self.assertEqual(cumulative_cash_flows, [-1000] * 5) + cash_pos = \ + [event['cumulative_perf']['ending_cash'] for event in results] + self.assertEqual(cash_pos, [9000] * 5) + def test_post_ex_long_position_receives_no_dividend(self): # post some trades in the market events = factory.create_trade_history( diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index c7f452e9..a9b38dec 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -75,7 +75,7 @@ import logbook import numpy as np import pandas as pd -from collections import OrderedDict, defaultdict +from collections import Counter, OrderedDict, defaultdict from six import iteritems, itervalues @@ -170,8 +170,19 @@ class PerformancePeriod(object): payment has been disbursed. """ cash_payments = 0.0 + stock_payments = Counter() # maps sid to number of shares paid for sid, pos in iteritems(self.positions): - cash_payments += pos.update_dividends(todays_date) + cash_payment, stock_payment = pos.update_dividends(todays_date) + cash_payments += cash_payment + stock_payments.update(stock_payment) + + for stock, payment in iteritems(stock_payments): + position = self.positions[stock] + position.amount += payment + self.ensure_position_index(stock) + self._position_amounts[stock] = position.amount + self._position_last_sale_prices[stock] = \ + position.last_sale_price # credit our cash balance with the dividend payments, or # if we are short, debit our cash balance with the diff --git a/zipline/finance/performance/position.py b/zipline/finance/performance/position.py index 3473e643..a641da12 100644 --- a/zipline/finance/performance/position.py +++ b/zipline/finance/performance/position.py @@ -36,6 +36,8 @@ from __future__ import division import logbook import math +from collections import Counter + log = logbook.Logger('Performance') @@ -57,28 +59,45 @@ class Position(object): This method will be invoked at the end of the market close handling, before the next market open. """ - payment = 0.0 + cash_payment = 0.0 + stock_payment = Counter() # maps sid to number of shares paid unpaid_dividends = [] for dividend in self.dividends: if midnight_utc == 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 - if dividend.net_amount: - dividend.payment = self.amount * dividend.net_amount + # stock dividend + if dividend.payment_sid: + # ie, 33.333 + raw_share_count = self.amount * float(dividend.ratio) + # ie, 33 + dividend.stock_payment = math.floor(raw_share_count) else: - dividend.payment = self.amount * dividend.gross_amount + dividend.stock_payment = None + # cash dividend + if dividend.net_amount: + dividend.cash_payment = self.amount * dividend.net_amount + elif dividend.gross_amount: + dividend.cash_payment = self.amount * dividend.gross_amount + else: + dividend.cash_payment = None if midnight_utc == dividend.pay_date: # if it is the payment date, include this # dividend's actual payment (calculated on # ex_date) - payment += dividend.payment + if dividend.stock_payment: + stock_payment[dividend.payment_sid] += \ + dividend.stock_payment + + if dividend.cash_payment: + cash_payment += dividend.cash_payment else: unpaid_dividends.append(dividend) self.dividends = unpaid_dividends - return payment + return cash_payment, stock_payment def add_dividend(self, dividend): self.dividends.append(dividend) diff --git a/zipline/utils/factory.py b/zipline/utils/factory.py index d1a8212f..7ff9edf4 100644 --- a/zipline/utils/factory.py +++ b/zipline/utils/factory.py @@ -138,10 +138,11 @@ def create_dividend(sid, payment, declared_date, ex_date, pay_date): 'sid': sid, 'gross_amount': payment, 'net_amount': payment, - 'dt': declared_date.replace(hour=0, minute=0, second=0, microsecond=0), - 'ex_date': ex_date.replace(hour=0, minute=0, second=0, microsecond=0), - 'pay_date': pay_date.replace(hour=0, minute=0, second=0, - microsecond=0), + 'payment_sid': None, + 'ratio': None, + 'dt': pd.tslib.normalize_date(declared_date), + 'ex_date': pd.tslib.normalize_date(ex_date), + 'pay_date': pd.tslib.normalize_date(pay_date), 'type': DATASOURCE_TYPE.DIVIDEND, 'source_id': 'MockDividendSource' }) @@ -149,6 +150,22 @@ def create_dividend(sid, payment, declared_date, ex_date, pay_date): return div +def create_stock_dividend(sid, payment_sid, ratio, declared_date, + ex_date, pay_date): + return Event({ + 'sid': sid, + 'payment_sid': payment_sid, + 'ratio': ratio, + 'net_amount': None, + 'gross_amount': None, + 'dt': pd.tslib.normalize_date(declared_date), + 'ex_date': pd.tslib.normalize_date(ex_date), + 'pay_date': pd.tslib.normalize_date(pay_date), + 'type': DATASOURCE_TYPE.DIVIDEND, + 'source_id': 'MockDividendSource' + }) + + def create_split(sid, ratio, date): return Event({ 'sid': sid,