mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-01 11:25:04 +08:00
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:
committed by
Eddie Hebert
parent
9ff588e7fc
commit
6fc077a573
@@ -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'])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user