From 191715a148244c0937b0eed284ef1ead0a4e66e8 Mon Sep 17 00:00:00 2001 From: John Ricklefs Date: Tue, 20 Aug 2013 17:10:24 -0400 Subject: [PATCH] ENH: Add support for asynchronous commission events. --- tests/test_perf_tracking.py | 38 ++++++++++++++++++++++++++++++++++ zipline/finance/blotter.py | 4 ++++ zipline/finance/performance.py | 37 ++++++++++++++++++++++++++++++++- zipline/protocol.py | 3 ++- zipline/utils/factory.py | 10 +++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index b1033ba0..9eaf8afa 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -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): diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index 7406a22f..a886f248 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -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: diff --git a/zipline/finance/performance.py b/zipline/finance/performance.py index 96b80d99..457e6222 100644 --- a/zipline/finance/performance.py +++ b/zipline/finance/performance.py @@ -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() diff --git a/zipline/protocol.py b/zipline/protocol.py index b32314eb..a6cc493d 100644 --- a/zipline/protocol.py +++ b/zipline/protocol.py @@ -29,7 +29,8 @@ DATASOURCE_TYPE = Enum( 'EMPTY', 'DONE', 'CUSTOM', - 'BENCHMARK' + 'BENCHMARK', + 'COMMISSION' ) diff --git a/zipline/utils/factory.py b/zipline/utils/factory.py index 123981b8..1545e21e 100644 --- a/zipline/utils/factory.py +++ b/zipline/utils/factory.py @@ -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