ENH: Add support for splits in zipline.

When a split is encountered, open positions and open orders
are updated accordingly.
This commit is contained in:
Jean Bredeche
2013-07-19 10:44:11 -07:00
committed by Eddie Hebert
parent 9ff588e7fc
commit 6fc077a573
6 changed files with 192 additions and 5 deletions
+35
View File
@@ -390,3 +390,38 @@ class FinanceTestCase(TestCase):
self.assertTrue(sid in oo)
order_list = oo[sid]
self.assertEqual(0, len(order_list))
def test_blotter_processes_splits(self):
sim_params = factory.create_simulation_parameters()
blotter = Blotter()
blotter.set_date(sim_params.period_start)
# set up two open limit orders with very low limit prices,
# one for sid 1 and one for sid 2
blotter.order(1, 100, 10, None, None)
blotter.order(2, 100, 10, None, None)
# send in a split for sid 2
split_event = factory.create_split(2, 0.33333,
sim_params.period_start +
timedelta(days=1))
blotter.process_split(split_event)
for sid in [1, 2]:
order_lists = blotter.open_orders[sid]
self.assertIsNotNone(order_lists)
self.assertEqual(1, len(order_lists))
aapl_order = blotter.open_orders[1][0].to_dict()
fls_order = blotter.open_orders[2][0].to_dict()
# make sure the aapl order didn't change
self.assertEqual(100, aapl_order['amount'])
self.assertEqual(10, aapl_order['limit'])
self.assertEqual(1, aapl_order['sid'])
# make sure the fls order did change
self.assertEqual(33, fls_order['amount'])
self.assertEqual(30, fls_order['limit'])
self.assertEqual(2, fls_order['sid'])
+57
View File
@@ -26,6 +26,7 @@ import itertools
import zipline.utils.factory as factory
import zipline.finance.performance as perf
from zipline.finance.slippage import Transaction, create_transaction
import zipline.utils.math_utils as zp_math
from zipline.gens.composites import date_sorted_sources
from zipline.finance.trading import SimulationParameters
@@ -86,6 +87,62 @@ def calculate_results(host, events):
return results
class TestSplitPerformance(unittest.TestCase):
def setUp(self):
self.sim_params, self.dt, self.end_dt = \
create_random_simulation_parameters()
# start with $10,000
self.sim_params.capital_base = 10e3
self.benchmark_events = benchmark_events_in_range(self.sim_params)
def test_split_long_position(self):
with trading.TradingEnvironment() as env:
events = factory.create_trade_history(
1,
[20, 20],
[100, 100],
oneday,
self.sim_params
)
# set up a long position in sid 1
# 100 shares at $20 apiece = $2000 position
events.insert(0, create_txn(events[0], 20, 100))
events.append(factory.create_split(1, 0.33333,
env.next_trading_day(events[1].dt)))
results = calculate_results(self, events)
# should have 33 shares (at $60 apiece) and $20 in cash
self.assertEqual(2, len(results))
latest_positions = results[1]['daily_perf']['positions']
self.assertEqual(1, len(latest_positions))
# check the last position to make sure it's been updated
position = latest_positions[0]
self.assertEqual(1, position['sid'])
self.assertEqual(33, position['amount'])
self.assertEqual(60, position['cost_basis'])
self.assertEqual(60, position['last_sale_price'])
# since we started with $10000, and we spent $2000 on the
# position, but then got $20 back, we should have $8020
# (or close to it) in cash.
# we won't get exactly 8020 because sometimes a split is
# denoted as a ratio like 0.3333, and we lose some digits
# of precision. thus, make sure we're pretty close.
daily_perf = results[1]['daily_perf']
self.assertTrue(
zp_math.tolerant_equals(8020,
daily_perf['ending_cash'], 1))
class TestDividendPerformance(unittest.TestCase):
def setUp(self):
+28
View File
@@ -142,6 +142,14 @@ class Blotter(object):
# along with newly placed orders.
self.new_orders.append(cur_order)
def process_split(self, split_event):
if split_event.sid not in self.open_orders:
return
orders_to_modify = self.open_orders[split_event.sid]
for order in orders_to_modify:
order.handle_split(split_event)
def process_trade(self, trade_event):
if trade_event.type != zp.DATASOURCE_TYPE.TRADE:
return
@@ -238,6 +246,26 @@ class Order(object):
self.stop_reached = stop_reached
self.limit_reached = limit_reached
def handle_split(self, split_event):
ratio = split_event.ratio
# update the amount, limit_price, and stop_price
# by the split's ratio
# info here: http://finra.complinet.com/en/display/display_plain.html?
# rbid=2403&element_id=8950&record_id=12208&print=1
# if we have an open order for 100 shares at $20, and we get
# a 3:1 split, we now want to have an open order for 33 shares at $60
# for the amount, we round down to the nearest whole share
self.amount = int(self.amount * ratio)
if self.limit:
self.limit = round(self.limit / ratio, 2)
if self.stop:
self.stop = round(self.stop / ratio, 2)
@property
def open(self):
if self.status == ORDER_STATUS.CANCELLED:
+57 -5
View File
@@ -304,6 +304,10 @@ class PerformanceTracker(object):
for perf_period in self.perf_periods:
perf_period.add_dividend(event)
elif event.type == zp.DATASOURCE_TYPE.SPLIT:
for perf_period in self.perf_periods:
perf_period.handle_split(event)
elif event.type == zp.DATASOURCE_TYPE.ORDER:
for perf_period in self.perf_periods:
perf_period.record_order(event)
@@ -407,7 +411,7 @@ class PerformanceTracker(object):
# increment the day counter before we move markers forward.
self.day_count += 1.0
# Take a snapshot of our current peformance to return to the
# Take a snapshot of our current performance to return to the
# browser.
daily_update = self.to_dict()
@@ -504,6 +508,43 @@ class Position(object):
def add_dividend(self, dividend):
self.dividends.append(dividend)
# Update the position by the split ratio, and return the
# resulting fractional share that will be converted into cash.
# Returns the unused cash.
def handle_split(self, split):
if (self.sid != split.sid):
raise NameError("updating split with the wrong sid!")
ratio = split.ratio
# adjust the # of shares by the ratio
# (if we had 100 shares, and the ratio is 0.33333,
# we now have 33 shares)
# ie, 33.333
raw_share_count = self.amount * ratio
# ie, 33
full_share_count = math.floor(raw_share_count)
# ie, 0.333
fractional_share_count = raw_share_count - full_share_count
# adjust the cost basis to the nearest cent, ie, 60.0
new_cost_basis = round(self.cost_basis / ratio, 2)
# adjust the last sale price
new_last_sale_price = round(self.last_sale_price / ratio, 2)
self.cost_basis = new_cost_basis
self.last_sale_price = new_last_sale_price
self.amount = full_share_count
# return the leftover cash, which will be converted into cash
# (rounded to the nearest cent)
return round(float(fractional_share_count * new_cost_basis), 2)
def update(self, txn):
if(self.sid != txn.sid):
raise NameError('updating position with txn for a different sid')
@@ -621,10 +662,18 @@ class PerformancePeriod(object):
# included in the event.
self.positions[div.sid].add_dividend(div)
def handle_split(self, split):
# Make the position object handle the split. It returns the
# leftover cash from a fractional share, if there is any.
leftover_cash = self.positions[split.sid].handle_split(split)
if leftover_cash > 0:
self.handle_cash_payment(leftover_cash)
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
to determine if we are owed a dividend payment or if the
payment has been disbursed.
"""
cash_payments = 0.0
@@ -634,16 +683,19 @@ class PerformancePeriod(object):
# credit our cash balance with the dividend payments, or
# if we are short, debit our cash balance with the
# payments.
self.period_cash_flow += cash_payments
# debit our cumulative cash spent with the dividend
# payments, or credit our cumulative cash spent if we are
# short the stock.
self.cumulative_capital_used -= cash_payments
self.handle_cash_payment(cash_payments)
# recalculate performance, including the dividend
# paymtents
# payments
self.calculate_performance()
def handle_cash_payment(self, payment_amount):
self.period_cash_flow += payment_amount
self.cumulative_capital_used -= payment_amount
def calculate_performance(self):
self.ending_value = self.calculate_positions_value()
+6
View File
@@ -104,6 +104,9 @@ class AlgorithmSimulator(object):
# and don't send a snapshot to handle_data.
if date < self.algo_start:
for event in snapshot:
if event.type == DATASOURCE_TYPE.SPLIT:
self.algo.blotter.process_split(event)
if event.type in (DATASOURCE_TYPE.TRADE,
DATASOURCE_TYPE.CUSTOM):
self.update_universe(event)
@@ -112,6 +115,9 @@ class AlgorithmSimulator(object):
else:
for event in snapshot:
if event.type == DATASOURCE_TYPE.SPLIT:
self.algo.blotter.process_split(event)
if event.type in (DATASOURCE_TYPE.TRADE,
DATASOURCE_TYPE.CUSTOM):
self.update_universe(event)
+9
View File
@@ -175,6 +175,15 @@ def create_dividend(sid, payment, declared_date, ex_date, pay_date):
return div
def create_split(sid, ratio, date):
return Event({
'sid': sid,
'ratio': ratio,
'dt': date.replace(hour=0, minute=0, second=0, microsecond=0),
'type': DATASOURCE_TYPE.SPLIT
})
def create_txn(sid, price, amount, datetime):
txn = Event({
'sid': sid,