ENH: Add support for asynchronous commission events.

This commit is contained in:
John Ricklefs
2013-08-20 17:10:24 -04:00
parent 57344ee78a
commit 191715a148
5 changed files with 90 additions and 2 deletions
+38
View File
@@ -145,6 +145,44 @@ class TestSplitPerformance(unittest.TestCase):
daily_perf['ending_cash'], 1))
class TestCommissionEvents(unittest.TestCase):
def setUp(self):
self.sim_params, self.dt, self.end_dt = \
create_random_simulation_parameters()
self.sim_params.capital_base = 10e3
self.benchmark_events = benchmark_events_in_range(self.sim_params)
def test_commission_event(self):
with trading.TradingEnvironment():
events = factory.create_trade_history(
1,
[10, 10, 10, 10, 10],
[100, 100, 100, 100, 100],
oneday,
self.sim_params
)
cash_adj_dt = self.sim_params.period_start \
+ datetime.timedelta(hours=3)
cash_adjustment = factory.create_commission(1, 300.0,
cash_adj_dt)
# Insert a purchase order.
events.insert(0, create_txn(events[0], 20, 1))
events.insert(1, cash_adjustment)
results = calculate_results(self, events)
# Validate that we lost 320 dollars from our cash pool.
self.assertEqual(results[-1]['cumulative_perf']['ending_cash'],
9680)
# Validate that the cost basis of our position changed.
self.assertEqual(results[-1]['daily_perf']['positions']
[0]['cost_basis'], 320.0)
class TestDividendPerformance(unittest.TestCase):
def setUp(self):
+4
View File
@@ -196,6 +196,10 @@ class Blotter(object):
return
for order, txn in self.transact(trade_event, current_orders):
if txn.type == zp.DATASOURCE_TYPE.COMMISSION:
yield txn, order
continue
if txn.amount == 0:
raise zipline.errors.TransactionWithNoAmount(txn=txn)
if math.copysign(1, txn.amount) != order.direction:
+36 -1
View File
@@ -130,6 +130,7 @@ omitted).
"""
from __future__ import division
import logbook
import math
@@ -312,6 +313,10 @@ class PerformanceTracker(object):
for perf_period in self.perf_periods:
perf_period.record_order(event)
elif event.type == zp.DATASOURCE_TYPE.COMMISSION:
for perf_period in self.perf_periods:
perf_period.handle_commission(event)
elif event.type == zp.DATASOURCE_TYPE.CUSTOM:
pass
elif event.type == zp.DATASOURCE_TYPE.BENCHMARK:
@@ -558,7 +563,8 @@ class Position(object):
def update(self, txn):
if(self.sid != txn.sid):
raise NameError('updating position with txn for a different sid')
raise Exception('updating position with txn for a '
'different sid')
# we're covering a short or closing a position
if(self.amount + txn.amount == 0):
@@ -572,6 +578,26 @@ class Position(object):
self.cost_basis = total_cost / total_shares
self.amount = self.amount + txn.amount
def adjust_commission_cost_basis(self, commission):
"""
A note about cost-basis in zipline: all positions are considered
to share a cost basis, even if they were executed in different
transactions with different commission costs, different prices, etc.
Due to limitations about how zipline handles positions, zipline will
currently spread an externally-delivered commission charge across
all shares in a position.
"""
if commission.sid != self.sid:
raise Exception('Updating a commission for a different sid?')
if commission.cost == 0.0:
return
prev_cost = self.cost_basis * self.amount
new_cost = prev_cost + commission.cost
self.cost_basis = new_cost / self.amount
def __repr__(self):
template = "sid: {sid}, amount: {amount}, cost_basis: {cost_basis}, \
last_sale_price: {last_sale_price}"
@@ -701,6 +727,15 @@ class PerformancePeriod(object):
self.period_cash_flow += payment_amount
self.cumulative_capital_used -= payment_amount
def handle_commission(self, commission):
# Deduct from our total cash pool.
negative_commission = math.copysign(commission.cost, -1.0)
self.handle_cash_payment(negative_commission)
# Adjust the cost basis of the stock if we own it
if commission.sid in self.positions:
self.positions[commission.sid].\
adjust_commission_cost_basis(commission)
def calculate_performance(self):
self.ending_value = self.calculate_positions_value()
+2 -1
View File
@@ -29,7 +29,8 @@ DATASOURCE_TYPE = Enum(
'EMPTY',
'DONE',
'CUSTOM',
'BENCHMARK'
'BENCHMARK',
'COMMISSION'
)
+10
View File
@@ -195,6 +195,16 @@ def create_txn(sid, price, amount, datetime):
return txn
def create_commission(sid, value, datetime):
txn = Event({
'dt': datetime,
'type': DATASOURCE_TYPE.COMMISSION,
'cost': value,
'sid': sid
})
return txn
def create_txn_history(sid, priceList, amtList, interval, sim_params):
txns = []
current = sim_params.first_open