From 8d275d8d83671765e9bb2fb948831d2fdef5a43f Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Wed, 19 Apr 2017 11:43:55 -0400 Subject: [PATCH 1/9] REF: Explicitly use Assets in Position, Order, Transaction (Instead of `sid`, which were already usually assets) Perf packets are unchanged and still emit `sid`: int --- tests/finance/test_blotter.py | 8 +- tests/finance/test_commissions.py | 8 +- tests/finance/test_slippage.py | 54 +++++------ tests/test_algorithm.py | 24 +++-- tests/test_finance.py | 39 ++++---- tests/test_perf_tracking.py | 59 ++++++----- zipline/finance/blotter.py | 68 ++++++------- zipline/finance/order.py | 28 +++--- zipline/finance/performance/period.py | 12 +-- zipline/finance/performance/position.py | 34 ++++--- .../finance/performance/position_tracker.py | 97 ++++++++++--------- zipline/finance/performance/tracker.py | 4 +- zipline/finance/transaction.py | 14 +-- zipline/protocol.py | 11 ++- zipline/utils/factory.py | 30 +++--- 15 files changed, 255 insertions(+), 235 deletions(-) diff --git a/tests/finance/test_blotter.py b/tests/finance/test_blotter.py index 2a031c81..adfc5eee 100644 --- a/tests/finance/test_blotter.py +++ b/tests/finance/test_blotter.py @@ -154,7 +154,7 @@ class BlotterTestCase(WithCreateBarData, blotter = Blotter('minute', self.asset_finder, cancel_policy=EODCancel()) - # Make two orders for the same sid, so we can test that we are not + # Make two orders for the same asset, so we can test that we are not # mutating the orders list as we are cancelling orders blotter.order(self.asset_24, 100, MarketOrder()) blotter.order(self.asset_24, -100, MarketOrder()) @@ -343,7 +343,7 @@ class BlotterTestCase(WithCreateBarData, other_order = Order( dt=blotter.current_dt, - sid=self.asset_25, + asset=self.asset_25, amount=1 ) @@ -406,7 +406,7 @@ class BlotterTestCase(WithCreateBarData, equity_txn = txns[0] self.assertEqual( equity_txn.price, - bar_data.current(equity_txn.sid, 'price'), + bar_data.current(equity_txn.asset, 'price'), ) self.assertEqual(commissions[0]['cost'], 1.0) @@ -416,6 +416,6 @@ class BlotterTestCase(WithCreateBarData, future_txn = txns[1] self.assertEqual( future_txn.price, - bar_data.current(future_txn.sid, 'price') + 1.0, + bar_data.current(future_txn.asset, 'price') + 1.0, ) self.assertEqual(commissions[1]['cost'], 2.0) diff --git a/tests/finance/test_commissions.py b/tests/finance/test_commissions.py index dacfe562..7bedb51a 100644 --- a/tests/finance/test_commissions.py +++ b/tests/finance/test_commissions.py @@ -21,16 +21,16 @@ class CommissionUnitTests(WithAssetFinder, ZiplineTestCase): asset1 = self.asset_finder.retrieve_asset(1) # one order - order = Order(dt=None, sid=asset1, amount=500) + order = Order(dt=None, asset=asset1, amount=500) # three fills - txn1 = Transaction(sid=asset1, amount=230, dt=None, + txn1 = Transaction(asset=asset1, amount=230, dt=None, price=100, order_id=order.id) - txn2 = Transaction(sid=asset1, amount=170, dt=None, + txn2 = Transaction(asset=asset1, amount=170, dt=None, price=101, order_id=order.id) - txn3 = Transaction(sid=asset1, amount=100, dt=None, + txn3 = Transaction(asset=asset1, amount=100, dt=None, price=102, order_id=order.id) return order, [txn1, txn2, txn3] diff --git a/tests/finance/test_slippage.py b/tests/finance/test_slippage.py index 5698080f..85b07a73 100644 --- a/tests/finance/test_slippage.py +++ b/tests/finance/test_slippage.py @@ -126,7 +126,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'limit': 3.5}) ] @@ -148,7 +148,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'limit': 3.5}) ] @@ -170,7 +170,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'limit': 3.6}) ] @@ -194,7 +194,7 @@ class SlippageTestCase(WithCreateBarData, # we ordered 100 shares, but default volume slippage only allows # for 2.5% of the volume. 2.5% * 2000 = 50 shares 'amount': int(50), - 'sid': int(133), + 'asset': self.ASSET133, 'order_id': open_orders[0].id } @@ -209,7 +209,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'limit': 3.5}) ] @@ -231,7 +231,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'limit': 3.5}) ] @@ -253,7 +253,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'limit': 3.4}) ] @@ -275,7 +275,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime( 2006, 1, 5, 14, 32, tzinfo=pytz.utc), 'amount': int(-50), - 'sid': int(133) + 'asset': self.ASSET133, } self.assertIsNotNone(txn) @@ -293,7 +293,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'stop': 4.0, 'limit': 3.0}) ] @@ -328,7 +328,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'stop': 4.0, 'limit': 3.5}) ] @@ -363,7 +363,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': 100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'stop': 4.0, 'limit': 3.6}) ] @@ -398,7 +398,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime( 2006, 1, 5, 14, 34, tzinfo=pytz.utc), 'amount': int(50), - 'sid': int(133) + 'asset': self.ASSET133 } for key, value in expected_txn.items(): @@ -411,7 +411,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'stop': 3.0, 'limit': 4.0}) ] @@ -446,7 +446,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'stop': 3.0, 'limit': 3.5}) ] @@ -481,7 +481,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), 'amount': -100, 'filled': 0, - 'sid': self.ASSET133, + 'asset': self.ASSET133, 'stop': 3.0, 'limit': 3.4}) ] @@ -516,7 +516,7 @@ class SlippageTestCase(WithCreateBarData, 'dt': datetime.datetime( 2006, 1, 5, 14, 32, tzinfo=pytz.utc), 'amount': int(-50), - 'sid': int(133) + 'asset': self.ASSET133, } for key, value in expected_txn.items(): @@ -574,7 +574,7 @@ class VolumeShareSlippageTestCase(WithCreateBarData, dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), amount=100, filled=0, - sid=self.ASSET133 + asset=self.ASSET133 ) ] @@ -596,7 +596,7 @@ class VolumeShareSlippageTestCase(WithCreateBarData, 'dt': datetime.datetime( 2006, 1, 5, 14, 31, tzinfo=pytz.utc), 'amount': int(5), - 'sid': int(133), + 'asset': self.ASSET133, 'commission': None, 'type': DATASOURCE_TYPE.TRANSACTION, 'order_id': open_orders[0].id @@ -613,7 +613,7 @@ class VolumeShareSlippageTestCase(WithCreateBarData, dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), amount=100, filled=0, - sid=self.ASSET133 + asset=self.ASSET133 ) ] @@ -684,7 +684,6 @@ class OrdersStopTestCase(WithSimParams, 'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'), 'amount': 100, 'filled': 0, - 'sid': 133, 'stop': 3.5 }, 'event': { @@ -693,7 +692,6 @@ class OrdersStopTestCase(WithSimParams, 'price': 4.0, 'high': 3.15, 'low': 2.85, - 'sid': 133, 'close': 4.0, 'open': 3.5 }, @@ -702,7 +700,6 @@ class OrdersStopTestCase(WithSimParams, 'price': 4.00025, 'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'), 'amount': 50, - 'sid': 133, } } }, @@ -711,7 +708,6 @@ class OrdersStopTestCase(WithSimParams, 'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'), 'amount': 100, 'filled': 0, - 'sid': 133, 'stop': 3.6 }, 'event': { @@ -720,7 +716,6 @@ class OrdersStopTestCase(WithSimParams, 'price': 3.5, 'high': 3.15, 'low': 2.85, - 'sid': 133, 'close': 3.5, 'open': 4.0 }, @@ -733,7 +728,6 @@ class OrdersStopTestCase(WithSimParams, 'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'), 'amount': -100, 'filled': 0, - 'sid': 133, 'stop': 3.4 }, 'event': { @@ -742,7 +736,6 @@ class OrdersStopTestCase(WithSimParams, 'price': 3.5, 'high': 3.15, 'low': 2.85, - 'sid': 133, 'close': 3.5, 'open': 3.0 }, @@ -755,7 +748,6 @@ class OrdersStopTestCase(WithSimParams, 'dt': pd.Timestamp('2006-01-05 14:30', tz='UTC'), 'amount': -100, 'filled': 0, - 'sid': 133, 'stop': 3.5 }, 'event': { @@ -764,7 +756,6 @@ class OrdersStopTestCase(WithSimParams, 'price': 3.0, 'high': 3.15, 'low': 2.85, - 'sid': 133, 'close': 3.0, 'open': 3.0 }, @@ -773,7 +764,6 @@ class OrdersStopTestCase(WithSimParams, 'price': 2.9998125, 'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'), 'amount': -50, - 'sid': 133, } } }, @@ -785,9 +775,13 @@ class OrdersStopTestCase(WithSimParams, ]) def test_orders_stop(self, name, order_data, event_data, expected): data = order_data - data['sid'] = self.ASSET133 + data['asset'] = self.ASSET133 order = Order(**data) + if expected['transaction']: + expected['transaction']['asset'] = self.ASSET133 + event_data['asset'] = self.ASSET133 + assets = ( (133, pd.DataFrame( { diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 8a907fd2..cb98d12a 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -3802,7 +3802,7 @@ class TestOrderCancelation(WithDataPortal, np.copysign(389, direction), daily_positions[0]["amount"], ) - self.assertEqual(1, results.positions[0][0]["sid"].sid) + self.assertEqual(1, results.positions[0][0]["sid"]) # should be an order on day1, but no more orders afterwards np.testing.assert_array_equal([1, 0, 0], @@ -4067,7 +4067,6 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): resources = self.make_data(auto_close_delta, 'daily', capital_base) assets = resources.assets - sids = [asset.sid for asset in assets] final_prices = resources.final_prices # Prices at which we expect our orders to be filled. @@ -4174,14 +4173,14 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): self.test_days[1] )[1] - for sid, txn in zip(sids, initial_fills): + for asset, txn in zip(assets, initial_fills): self.assertDictContainsSubset( { 'amount': order_size, 'commission': None, 'dt': last_minute_of_session, - 'price': initial_fill_prices[sid], - 'sid': sid, + 'price': initial_fill_prices[asset], + 'sid': asset, }, txn, ) @@ -4203,7 +4202,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): 'commission': 0.0, 'dt': assets[0].auto_close_date, 'price': fp0, - 'sid': sids[0], + 'sid': assets[0], 'order_id': None, # Auto-close txns emit Nones for order_id. }, ) @@ -4218,7 +4217,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): 'commission': 0.0, 'dt': assets[1].auto_close_date, 'price': fp1, - 'sid': sids[1], + 'sid': assets[1], 'order_id': None, # Auto-close txns emit Nones for order_id. }, ) @@ -4313,7 +4312,6 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): env = resources.env assets = resources.assets - sids = [a.sid for a in assets] final_prices = resources.final_prices backtest_minutes = resources.trade_data_by_sid[0].index.tolist() @@ -4390,14 +4388,14 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): # the backtest, which is still on the first day in minute mode. initial_fills = transactions.iloc[0] self.assertEqual(len(initial_fills), len(assets)) - for sid, txn in zip(sids, initial_fills): + for asset, txn in zip(assets, initial_fills): self.assertDictContainsSubset( { 'amount': order_size, 'commission': None, 'dt': backtest_minutes[1], - 'price': initial_fill_prices[sid], - 'sid': sid, + 'price': initial_fill_prices[asset], + 'sid': asset, }, txn, ) @@ -4419,7 +4417,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): 'commission': 0.0, 'dt': assets[0].auto_close_date, 'price': fp0, - 'sid': sids[0], + 'sid': assets[0], 'order_id': None, # Auto-close txns emit Nones for order_id. }, ) @@ -4434,7 +4432,7 @@ class TestEquityAutoClose(WithTmpDir, WithTradingCalendars, ZiplineTestCase): 'commission': 0.0, 'dt': assets[1].auto_close_date, 'price': fp1, - 'sid': sids[1], + 'sid': assets[1], 'order_id': None, # Auto-close txns emit Nones for order_id. }, ) diff --git a/tests/test_finance.py b/tests/test_finance.py index 5b72449f..f176d608 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -187,8 +187,8 @@ class FinanceTestCase(WithLogger, # if present, expect transaction amounts to match orders exactly. complete_fill = params.get('complete_fill') - sid = 1 - metadata = make_simple_equity_info([sid], self.start, self.end) + asset1 = self.asset_finder.retrieve_asset(1) + metadata = make_simple_equity_info([asset1.sid], self.start, self.end) with TempDirectory() as tempdir, \ tmp_trading_env(equities=metadata) as env: @@ -206,7 +206,7 @@ class FinanceTestCase(WithLogger, price_data = np.array([10.1] * len(minutes)) assets = { - sid: pd.DataFrame({ + asset1.sid: pd.DataFrame({ "open": price_data, "high": price_data, "low": price_data, @@ -305,7 +305,7 @@ class FinanceTestCase(WithLogger, # place an order direction = alternator ** len(order_list) order_id = blotter.order( - blotter.asset_finder.retrieve_asset(sid), + asset1, order_amount * direction, MarketOrder()) order_list.append(blotter.orders[order_id]) @@ -333,7 +333,7 @@ class FinanceTestCase(WithLogger, for i in range(order_count): order = order_list[i] - self.assertEqual(order.sid, sid) + self.assertEqual(order.asset, asset1) self.assertEqual(order.amount, order_amount * alternator ** i) if complete_fill: @@ -351,15 +351,19 @@ class FinanceTestCase(WithLogger, self.assertEqual(len(transactions), expected_txn_count) - cumulative_pos = tracker.position_tracker.positions[sid] + cumulative_pos = tracker.position_tracker.positions[asset1] if total_volume == 0: self.assertIsNone(cumulative_pos) else: self.assertEqual(total_volume, cumulative_pos.amount) - # the open orders should not contain sid. + # the open orders should not contain the asset. oo = blotter.open_orders - self.assertNotIn(sid, oo, "Entry is removed when no open orders") + self.assertNotIn( + asset1, + oo, + "Entry is removed when no open orders" + ) def test_blotter_processes_splits(self): blotter = Blotter('daily', self.env.asset_finder, @@ -376,23 +380,24 @@ class FinanceTestCase(WithLogger, blotter.process_splits([(2, 0.3333)]) for sid in [1, 2]: - order_lists = blotter.open_orders[sid] + order_lists = \ + blotter.open_orders[blotter.asset_finder.retrieve_asset(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() + aapl_order = blotter.open_orders[1][0] + fls_order = blotter.open_orders[2][0] # 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']) + self.assertEqual(100, aapl_order.amount) + self.assertEqual(10, aapl_order.limit) + self.assertEqual(1, aapl_order.asset) # make sure the fls order did change # to 300 shares at 3.33 - self.assertEqual(300, fls_order['amount']) - self.assertEqual(3.33, fls_order['limit']) - self.assertEqual(2, fls_order['sid']) + self.assertEqual(300, fls_order.amount) + self.assertEqual(3.33, fls_order.limit) + self.assertEqual(2, fls_order.asset) class TradingEnvironmentTestCase(WithLogger, diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index f76f9ee5..f0ac8af6 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -347,7 +347,7 @@ class TestSplitPerformance(WithSimParams, WithTmpDir, ZiplineTestCase): # check the last position to make sure it's been updated position = latest_positions[0] - self.assertEqual(1, position['sid']) + self.assertEqual(self.asset1, position['sid']) self.assertEqual(33, position['amount']) self.assertEqual(60, position['cost_basis']) self.assertEqual(60, position['last_sale_price']) @@ -1326,8 +1326,8 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, "should be just one position") self.assertEqual( - pp.positions[1].sid, - txn.sid, + pp.positions[1].asset, + txn.asset, "position should be in security with id 1") self.assertEqual( @@ -1437,8 +1437,8 @@ single short-sale transaction""" "should be just one position") self.assertEqual( - pp.positions[1].sid, - txn.sid, + pp.positions[1].asset, + txn.asset, "position should be in security from the transaction" ) @@ -1494,8 +1494,8 @@ single short-sale transaction""" ) self.assertEqual( - pp.positions[1].sid, - txn.sid, + pp.positions[1].asset, + txn.asset, "position should be in security from the transaction" ) @@ -1556,8 +1556,8 @@ cost of sole txn in test" "should be just one position" ) self.assertEqual( - ppTotal.positions[1].sid, - txn.sid, + ppTotal.positions[1].asset, + txn.asset, "position should be in security from the transaction" ) @@ -1834,7 +1834,7 @@ shares in position" self.create_environment_stuff(num_days=8) history_args = ( - 1, + self.asset1, [10, 9, 11, 8, 9, 12, 13, 14], [200, -100, -100, 100, -300, 100, 500, 400], oneday, @@ -1973,6 +1973,16 @@ class TestPositionTracker(WithTradingEnvironment, ZiplineTestCase): ASSET_FINDER_EQUITY_SIDS = 1, 2 + @classmethod + def init_class_fixtures(cls): + super(TestPositionTracker, cls).init_class_fixtures() + + cls.EQUITY1 = cls.asset_finder.retrieve_asset(1) + cls.EQUITY2 = cls.asset_finder.retrieve_asset(2) + cls.FUTURE3 = cls.asset_finder.retrieve_asset(3) + cls.FUTURE4 = cls.asset_finder.retrieve_asset(4) + cls.FUTURE5 = cls.asset_finder.retrieve_asset(1032201401) + @classmethod def make_futures_info(cls): return pd.DataFrame.from_dict( @@ -2017,13 +2027,13 @@ class TestPositionTracker(WithTradingEnvironment, def test_position_values_and_exposures(self): pt = perf.PositionTracker(self.env.asset_finder, None) dt = pd.Timestamp("1984/03/06 3:00PM") - pos1 = perf.Position(1, amount=np.float64(10.0), + pos1 = perf.Position(self.EQUITY1, amount=np.float64(10.0), last_sale_date=dt, last_sale_price=10) - pos2 = perf.Position(2, amount=np.float64(-20.0), + pos2 = perf.Position(self.EQUITY2, amount=np.float64(-20.0), last_sale_date=dt, last_sale_price=10) - pos3 = perf.Position(3, amount=np.float64(30.0), + pos3 = perf.Position(self.FUTURE3, amount=np.float64(30.0), last_sale_date=dt, last_sale_price=10) - pos4 = perf.Position(4, amount=np.float64(-40.0), + pos4 = perf.Position(self.FUTURE4, amount=np.float64(-40.0), last_sale_date=dt, last_sale_price=10) pt.update_positions({1: pos1, 2: pos2, 3: pos3, 4: pos4}) @@ -2049,11 +2059,11 @@ class TestPositionTracker(WithTradingEnvironment, def test_update_positions(self): pt = perf.PositionTracker(self.env.asset_finder, None) dt = pd.Timestamp("2014/01/01 3:00PM") - pos1 = perf.Position(1, amount=np.float64(10.0), + pos1 = perf.Position(self.EQUITY1, amount=np.float64(10.0), last_sale_date=dt, last_sale_price=10) - pos2 = perf.Position(2, amount=np.float64(-20.0), + pos2 = perf.Position(self.EQUITY2, amount=np.float64(-20.0), last_sale_date=dt, last_sale_price=10) - pos3 = perf.Position(1032201401, amount=np.float64(30.0), + pos3 = perf.Position(self.FUTURE5, amount=np.float64(30.0), last_sale_date=dt, last_sale_price=100) # Call update_positions twice. When the second call is made, @@ -2063,8 +2073,8 @@ class TestPositionTracker(WithTradingEnvironment, # were to be stored as a dict, then its order could change in arbitrary # ways when the second update_positions call is made. Hence we also # store it as an OrderedDict. - pt.update_positions({1: pos1, 1032201401: pos3}) - pt.update_positions({2: pos2}) + pt.update_positions({self.EQUITY1: pos1, self.FUTURE5: pos3}) + pt.update_positions({self.EQUITY2: pos2}) pos_stats = pt.stats() # Test long-only methods @@ -2087,19 +2097,17 @@ class TestPositionTracker(WithTradingEnvironment, self.assertEqual(100 + 150000 - 200, pos_stats.net_exposure) def test_close_position(self): - future_sid = 1032201401 - equity_sid = 1 pt = perf.PositionTracker(self.env.asset_finder, None) dt = pd.Timestamp('2017/01/04 3:00PM') pos1 = perf.Position( - sid=future_sid, + asset=self.FUTURE5, amount=np.float64(30.0), last_sale_date=dt, last_sale_price=100, ) pos2 = perf.Position( - sid=equity_sid, + asset=self.EQUITY1, amount=np.float64(10.0), last_sale_date=dt, last_sale_price=10, @@ -2110,10 +2118,9 @@ class TestPositionTracker(WithTradingEnvironment, # OrderedDicts. If `future_sid` is not removed from the multipliers # dictionaries, equities will hit the incorrect multiplier when # computing `pt.stats()`. - pt.update_positions({future_sid: pos1, equity_sid: pos2}) + pt.update_positions({self.FUTURE5: pos1, self.EQUITY1: pos2}) - asset_to_close = self.env.asset_finder.retrieve_asset(future_sid) - txn = create_txn(asset_to_close, dt, 100, -30) + txn = create_txn(self.FUTURE5, dt, 100, -30) pt.execute_transaction(txn) pos_stats = pt.stats() diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index 9a61e06a..9ca934b6 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -18,7 +18,7 @@ from copy import copy from six import iteritems -from zipline.assets import Equity, Future +from zipline.assets import Equity, Future, Asset from zipline.finance.order import Order from zipline.finance.slippage import VolumeShareSlippage from zipline.finance.commission import ( @@ -27,6 +27,7 @@ from zipline.finance.commission import ( PerTrade, ) from zipline.finance.cancel_policy import NeverCancel +from zipline.utils.input_validation import expect_types log = Logger('Blotter') warning_logger = Logger('AlgoWarning') @@ -36,15 +37,12 @@ class Blotter(object): def __init__(self, data_frequency, asset_finder, equity_slippage=None, future_slippage=None, equity_commission=None, future_commission=None, cancel_policy=None): - # these orders are aggregated by sid + # these orders are aggregated by asset self.open_orders = defaultdict(list) # keep a dict of orders by their own id self.orders = {} - # all our legacy order management code works with integer sids. - # this lets us convert those to assets when needed. ideally, we'd just - # revamp all the legacy code to work with assets. self.asset_finder = asset_finder # holding orders that have come in since the last event. @@ -88,7 +86,8 @@ class Blotter(object): def set_date(self, dt): self.current_dt = dt - def order(self, sid, amount, style, order_id=None): + @expect_types(asset=Asset) + def order(self, asset, amount, style, order_id=None): """Place an order. Parameters @@ -114,10 +113,10 @@ class Blotter(object): ----- amount > 0 :: Buy/Cover amount < 0 :: Sell/Short - Market order: order(sid, amount) - Limit order: order(sid, amount, style=LimitOrder(limit_price)) - Stop order: order(sid, amount, style=StopOrder(stop_price)) - StopLimit order: order(sid, amount, style=StopLimitOrder(limit_price, + Market order: order(asset, amount) + Limit order: order(asset, amount, style=LimitOrder(limit_price)) + Stop order: order(asset, amount, style=StopOrder(stop_price)) + StopLimit order: order(asset, amount, style=StopLimitOrder(limit_price, stop_price)) """ # something could be done with amount to further divide @@ -136,14 +135,14 @@ class Blotter(object): is_buy = (amount > 0) order = Order( dt=self.current_dt, - sid=sid, + asset=asset, amount=amount, stop=style.get_stop_price(is_buy), limit=style.get_limit_price(is_buy), id=order_id ) - self.open_orders[order.sid].append(order) + self.open_orders[order.asset].append(order) self.orders[order.id] = order self.new_orders.append(order) @@ -177,7 +176,7 @@ class Blotter(object): cur_order = self.orders[order_id] if cur_order.open: - order_list = self.open_orders[cur_order.sid] + order_list = self.open_orders[cur_order.asset] if cur_order in order_list: order_list.remove(cur_order) @@ -217,7 +216,7 @@ class Blotter(object): 'filled by the end of day and ' 'were canceled.'.format( order_amt=order.amount, - order_sym=order.sid.symbol, + order_sym=order.asset.symbol, order_filled=order.filled, order_failed=order.amount - order.filled, ) @@ -231,7 +230,7 @@ class Blotter(object): 'filled by the end of day and ' 'were canceled.'.format( order_amt=order.amount, - order_sym=order.sid.symbol, + order_sym=order.asset.symbol, order_filled=-1 * order.filled, order_failed=-1 * (order.amount - order.filled), ) @@ -242,7 +241,7 @@ class Blotter(object): '{order_sym} failed to fill by the end of day ' 'and was canceled.'.format( order_amt=order.amount, - order_sym=order.sid.symbol, + order_sym=order.asset.symbol, ) ) @@ -268,7 +267,7 @@ class Blotter(object): cur_order = self.orders[order_id] - order_list = self.open_orders[cur_order.sid] + order_list = self.open_orders[cur_order.asset] if cur_order in order_list: order_list.remove(cur_order) @@ -306,20 +305,19 @@ class Blotter(object): Parameters ---------- splits: list - A list of splits. Each split is a tuple of (sid, ratio). + A list of splits. Each split is a tuple of (asset, ratio). Returns ------- None """ - for split in splits: - sid = split[0] - if sid not in self.open_orders: + for asset, ratio in splits: + if asset not in self.open_orders: return - orders_to_modify = self.open_orders[sid] + orders_to_modify = self.open_orders[asset] for order in orders_to_modify: - order.handle_split(split[1]) + order.handle_split(ratio) def get_transactions(self, bar_data): """ @@ -344,7 +342,7 @@ class Blotter(object): commissions_list: List commissions_list: list of commissions resulting from filling the - open orders. A commission is an object with "sid" and "cost" + open orders. A commission is an object with "asset" and "cost" parameters. closed_orders: List @@ -356,11 +354,7 @@ class Blotter(object): commissions = [] if self.open_orders: - assets = self.asset_finder.retrieve_all(self.open_orders) - asset_dict = {asset.sid: asset for asset in assets} - - for sid, asset_orders in iteritems(self.open_orders): - asset = asset_dict[sid] + for asset, asset_orders in iteritems(self.open_orders): slippage = self.slippage_models[type(asset)] for order, txn in \ @@ -370,7 +364,7 @@ class Blotter(object): if additional_commission > 0: commissions.append({ - "sid": order.sid, + "asset": order.asset, "order": order, "cost": additional_commission }) @@ -401,15 +395,15 @@ class Blotter(object): """ # remove all closed orders from our open_orders dict for order in closed_orders: - sid = order.sid - sid_orders = self.open_orders[sid] + asset = order.asset + asset_orders = self.open_orders[asset] try: - sid_orders.remove(order) + asset_orders.remove(order) except ValueError: continue - # now clear out the sids from our open_orders dict that have + # now clear out the assets from our open_orders dict that have # zero open orders - for sid in list(self.open_orders.keys()): - if len(self.open_orders[sid]) == 0: - del self.open_orders[sid] + for asset in list(self.open_orders.keys()): + if len(self.open_orders[asset]) == 0: + del self.open_orders[asset] diff --git a/zipline/finance/order.py b/zipline/finance/order.py index d326f206..91b3edae 100644 --- a/zipline/finance/order.py +++ b/zipline/finance/order.py @@ -20,6 +20,7 @@ from six import text_type import zipline.protocol as zp from zipline.assets import Asset from zipline.utils.enum import enum +from zipline.utils.input_validation import expect_types ORDER_STATUS = enum( 'OPEN', @@ -34,35 +35,35 @@ BUY = 1 << 1 STOP = 1 << 2 LIMIT = 1 << 3 -ORDER_FIELDS_TO_IGNORE = {'type', 'direction', '_status'} +ORDER_FIELDS_TO_IGNORE = {'type', 'direction', '_status', 'asset'} class Order(object): # using __slots__ to save on memory usage. Simulations can create many # Order objects and we keep them all in memory, so it's worthwhile trying # to cut down on the memory footprint of this object. - __slots__ = ["id", "dt", "reason", "created", "sid", "amount", "filled", + __slots__ = ["id", "dt", "reason", "created", "asset", "amount", "filled", "commission", "_status", "stop", "limit", "stop_reached", "limit_reached", "direction", "type", "broker_order_id"] - def __init__(self, dt, sid, amount, stop=None, limit=None, filled=0, + @expect_types(asset=Asset) + def __init__(self, dt, asset, amount, stop=None, limit=None, filled=0, commission=0, id=None): """ @dt - datetime.datetime that the order was placed - @sid - asset for the order. called sid for historical reasons. + @asset - asset for the order. @amount - the number of shares to buy/sell a positive sign indicates a buy a negative sign indicates a sell @filled - how many shares of the order have been filled so far """ - assert isinstance(sid, Asset) # get a string representation of the uuid. self.id = self.make_id() if id is None else id self.dt = dt self.reason = None self.created = dt - self.sid = sid + self.asset = asset self.amount = amount self.filled = filled self.commission = commission @@ -86,9 +87,16 @@ class Order(object): if self.broker_order_id is None: del dct['broker_order_id'] + dct['sid'] = self.asset dct['status'] = self.status return dct + @property + def sid(self): + # For backwards compatibility because we pass this object to + # custom slippage models. + return self.asset + def to_api_obj(self): pydict = self.to_dict() obj = zp.Order(initial_values=pydict) @@ -215,14 +223,6 @@ class Order(object): def open(self): return self.status in [ORDER_STATUS.OPEN, ORDER_STATUS.HELD] - @property - def asset(self): - """ - Convenience accessor to hide away a historical API that we'd like to - change at some point. - """ - return self.sid - @property def triggered(self): """ diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index cc17f374..1a0d8d70 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -185,8 +185,8 @@ class PerformancePeriod(object): self._account_store = zp.Account() self.serialize_positions = serialize_positions - # This dict contains the known cash flow multipliers for sids and is - # keyed on sid + # This dict contains the known cash flow multipliers for assets and is + # keyed on asset self._execution_cash_flow_multipliers = {} _position_tracker = None @@ -362,7 +362,7 @@ class PerformancePeriod(object): def handle_execution(self, txn): self.cash_flow += self._calculate_execution_cash_flow(txn) - asset = self.asset_finder.retrieve_asset(txn.sid) + asset = self.asset_finder.retrieve_asset(txn.asset) if isinstance(asset, Future): try: old_price = self._payout_last_sale_prices[asset] @@ -392,15 +392,15 @@ class PerformancePeriod(object): # Check if the multiplier is cached. If it is not, look up the asset # and cache the multiplier. try: - multiplier = self._execution_cash_flow_multipliers[txn.sid] + multiplier = self._execution_cash_flow_multipliers[txn.asset] except KeyError: - asset = self.asset_finder.retrieve_asset(txn.sid) + asset = self.asset_finder.retrieve_asset(txn.asset) # Futures experience no cash flow on transactions if isinstance(asset, Future): multiplier = 0 else: multiplier = 1 - self._execution_cash_flow_multipliers[txn.sid] = multiplier + self._execution_cash_flow_multipliers[txn.asset] = multiplier # Calculate and return the cash flow given the multiplier return -1 * txn.price * txn.amount * multiplier diff --git a/zipline/finance/performance/position.py b/zipline/finance/performance/position.py index 7c818d59..24bc5bb4 100644 --- a/zipline/finance/performance/position.py +++ b/zipline/finance/performance/position.py @@ -20,7 +20,7 @@ Position Tracking +-----------------+----------------------------------------------------+ | key | value | +=================+====================================================+ - | sid | the sid for the asset held in this position | + | asset | the asset held in this position | +-----------------+----------------------------------------------------+ | amount | whole number of shares in the position | +-----------------+----------------------------------------------------+ @@ -37,15 +37,19 @@ from collections import OrderedDict import numpy as np import logbook +from zipline.assets import Future, Asset +from zipline.utils.input_validation import expect_types + log = logbook.Logger('Performance') class Position(object): - def __init__(self, sid, amount=0, cost_basis=0.0, + @expect_types(asset=Asset) + def __init__(self, asset, amount=0, cost_basis=0.0, last_sale_price=0.0, last_sale_date=None): - self.sid = sid + self.asset = asset self.amount = amount self.cost_basis = cost_basis # per share self.last_sale_price = last_sale_price @@ -72,15 +76,16 @@ class Position(object): ) } - def handle_split(self, sid, ratio): + @expect_types(asset=Asset) + def handle_split(self, asset, ratio): """ Update the position by the split ratio, and return the resulting fractional share that will be converted into cash. Returns the unused cash. """ - if self.sid != sid: - raise Exception("updating split with the wrong sid!") + if self.asset != asset: + raise Exception("updating split with the wrong asset!") # adjust the # of shares by the ratio # (if we had 100 shares, and the ratio is 3, @@ -113,9 +118,9 @@ class Position(object): return return_cash def update(self, txn): - if self.sid != txn.sid: + if self.asset != txn.asset: raise Exception('updating position with txn for a ' - 'different sid') + 'different asset') total_shares = self.amount + txn.amount @@ -145,7 +150,8 @@ class Position(object): self.amount = total_shares - def adjust_commission_cost_basis(self, sid, cost): + @expect_types(asset=Asset) + def adjust_commission_cost_basis(self, asset, cost): """ A note about cost-basis in zipline: all positions are considered to share a cost basis, even if they were executed in different @@ -156,8 +162,8 @@ class Position(object): all shares in a position. """ - if sid != self.sid: - raise Exception('Updating a commission for a different sid?') + if asset != self.asset: + raise Exception('Updating a commission for a different asset?') if cost == 0.0: return @@ -171,10 +177,10 @@ class Position(object): self.cost_basis = new_cost / self.amount def __repr__(self): - template = "sid: {sid}, amount: {amount}, cost_basis: {cost_basis}, \ + template = "asset: {asset}, amount: {amount}, cost_basis: {cost_basis}, \ last_sale_price: {last_sale_price}" return template.format( - sid=self.sid, + asset=self.asset, amount=self.amount, cost_basis=self.cost_basis, last_sale_price=self.last_sale_price @@ -186,7 +192,7 @@ last_sale_price: {last_sale_price}" Returns a dict object of the form: """ return { - 'sid': self.sid, + 'sid': self.asset, 'amount': self.amount, 'cost_basis': self.cost_basis, 'last_sale_price': self.last_sale_price diff --git a/zipline/finance/performance/position_tracker.py b/zipline/finance/performance/position_tracker.py index 417c7392..0f8edc25 100644 --- a/zipline/finance/performance/position_tracker.py +++ b/zipline/finance/performance/position_tracker.py @@ -19,8 +19,6 @@ import logbook import numpy as np from collections import namedtuple from math import isnan -from zipline.finance.performance.position import Position -from zipline.finance.transaction import Transaction try: # optional cython based OrderedDict @@ -29,9 +27,14 @@ except ImportError: from collections import OrderedDict from six import iteritems, itervalues +from zipline.finance.performance.position import Position +from zipline.finance.transaction import Transaction +from zipline.utils.input_validation import expect_types import zipline.protocol as zp from zipline.assets import ( - Equity, Future + Equity, + Future, + Asset ) from zipline.errors import PositionTrackerMissingAssetFinder from . position import positiondict @@ -122,7 +125,7 @@ class PositionTracker(object): def __init__(self, asset_finder, data_frequency): self.asset_finder = asset_finder - # sid => position object + # asset => position object self.positions = positiondict() # Arrays for quick calculations of positions value self._position_value_multipliers = OrderedDict() @@ -133,41 +136,42 @@ class PositionTracker(object): self.data_frequency = data_frequency - def _update_asset(self, sid): + def _update_asset(self, asset): try: - self._position_value_multipliers[sid] - self._position_exposure_multipliers[sid] + self._position_value_multipliers[asset] + self._position_exposure_multipliers[asset] except KeyError: # Check if there is an AssetFinder if self.asset_finder is None: raise PositionTrackerMissingAssetFinder() - # Collect the value multipliers from applicable sids - asset = self.asset_finder.retrieve_asset(sid) + # Collect the value multipliers from applicable assets + asset = self.asset_finder.retrieve_asset(asset) if isinstance(asset, Equity): - self._position_value_multipliers[sid] = 1 - self._position_exposure_multipliers[sid] = 1 + self._position_value_multipliers[asset] = 1 + self._position_exposure_multipliers[asset] = 1 if isinstance(asset, Future): - self._position_value_multipliers[sid] = 0 - self._position_exposure_multipliers[sid] = asset.multiplier + self._position_value_multipliers[asset] = 0 + self._position_exposure_multipliers[asset] = asset.multiplier def update_positions(self, positions): # update positions in batch self.positions.update(positions) - for sid, pos in iteritems(positions): - self._update_asset(sid) + for asset, pos in iteritems(positions): + self._update_asset(asset) - def update_position(self, sid, amount=None, last_sale_price=None, + @expect_types(asset=Asset) + def update_position(self, asset, amount=None, last_sale_price=None, last_sale_date=None, cost_basis=None): - if sid not in self.positions: - position = Position(sid) - self.positions[sid] = position + if asset not in self.positions: + position = Position(asset) + self.positions[asset] = position else: - position = self.positions[sid] + position = self.positions[asset] if amount is not None: position.amount = amount - self._update_asset(sid=sid) + self._update_asset(asset=asset) if last_sale_price is not None: position.last_sale_price = last_sale_price if last_sale_date is not None: @@ -178,36 +182,37 @@ class PositionTracker(object): def execute_transaction(self, txn): # Update Position # ---------------- - sid = txn.sid + asset = txn.asset - if sid not in self.positions: - position = Position(sid) - self.positions[sid] = position + if asset not in self.positions: + position = Position(asset) + self.positions[asset] = position else: - position = self.positions[sid] + position = self.positions[asset] position.update(txn) if position.amount == 0: # if this position now has 0 shares, remove it from our internal # bookkeeping. - del self.positions[sid] - del self._position_value_multipliers[sid] - del self._position_exposure_multipliers[sid] + del self.positions[asset] + del self._position_value_multipliers[asset] + del self._position_exposure_multipliers[asset] try: # if this position exists in our user-facing dictionary, # remove it as well. - del self._positions_store[sid] + del self._positions_store[asset] except KeyError: pass else: - self._update_asset(sid) + self._update_asset(asset) - def handle_commission(self, sid, cost): + @expect_types(asset=Asset) + def handle_commission(self, asset, cost): # Adjust the cost basis of the stock if we own it - if sid in self.positions: - self.positions[sid].adjust_commission_cost_basis(sid, cost) + if asset in self.positions: + self.positions[asset].adjust_commission_cost_basis(asset, cost) def handle_splits(self, splits): """ @@ -216,7 +221,8 @@ class PositionTracker(object): Parameters ---------- splits: list - A list of splits. Each split is a tuple of (sid, ratio). + A list of splits. Each split is a tuple of (sid, ratio), where + sid is an integer. Returns ------- @@ -227,11 +233,12 @@ class PositionTracker(object): for split in splits: sid = split[0] - if sid in self.positions: + asset = self.asset_finder.retrieve_asset(sid) + if asset in self.positions: # Make the position object handle the split. It returns the # leftover cash from a fractional share, if there is any. - position = self.positions[sid] - leftover_cash = position.handle_split(sid, split[1]) + position = self.positions[asset] + leftover_cash = position.handle_split(asset, split[1]) self._update_asset(split[0]) total_leftover_cash += leftover_cash @@ -326,7 +333,7 @@ class PositionTracker(object): price = self.positions.get(asset).last_sale_price txn = Transaction( - sid=asset, + asset=asset, amount=(-1 * amount), dt=dt, price=price, @@ -339,20 +346,20 @@ class PositionTracker(object): positions = self._positions_store - for sid, pos in iteritems(self.positions): + for asset, pos in iteritems(self.positions): if pos.amount == 0: # Clear out the position if it has become empty since the last # time get_positions was called. Catching the KeyError is - # faster than checking `if sid in positions`, and this can be + # faster than checking `if asset in positions`, and this can be # potentially called in a tight inner loop. try: - del positions[sid] + del positions[asset] except KeyError: pass continue - position = zp.Position(sid) + position = zp.Position(asset) position.amount = pos.amount position.cost_basis = pos.cost_basis position.last_sale_price = pos.last_sale_price @@ -360,13 +367,13 @@ class PositionTracker(object): # Adds the new position if we didn't have one before, or overwrite # one we have currently - positions[sid] = position + positions[asset] = position return positions def get_positions_list(self): positions = [] - for sid, pos in iteritems(self.positions): + for asset, pos in iteritems(self.positions): if pos.amount != 0: positions.append(pos.to_dict()) return positions diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index ffef7ecd..c7db49ee 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -275,10 +275,10 @@ class PerformanceTracker(object): self.todays_performance.record_order(event) def process_commission(self, commission): - sid = commission['sid'] + asset = commission['asset'] cost = commission['cost'] - self.position_tracker.handle_commission(sid, cost) + self.position_tracker.handle_commission(asset, cost) self.cumulative_performance.handle_commission(cost) self.todays_performance.handle_commission(cost) diff --git a/zipline/finance/transaction.py b/zipline/finance/transaction.py index dcd2fce2..55814238 100644 --- a/zipline/finance/transaction.py +++ b/zipline/finance/transaction.py @@ -18,14 +18,13 @@ from copy import copy from zipline.assets import Asset from zipline.protocol import DATASOURCE_TYPE +from zipline.utils.input_validation import expect_types class Transaction(object): - - def __init__(self, sid, amount, dt, price, order_id, commission=None): - assert isinstance(sid, Asset) - - self.sid = sid + @expect_types(asset=Asset) + def __init__(self, asset, amount, dt, price, order_id, commission=None): + self.asset = asset self.amount = amount self.dt = dt self.price = price @@ -39,6 +38,9 @@ class Transaction(object): def to_dict(self): py = copy(self.__dict__) del py['type'] + del py['asset'] + py['sid'] = self.asset + return py @@ -53,7 +55,7 @@ def create_transaction(order, dt, price, amount): raise Exception("Transaction magnitude must be at least 1.") transaction = Transaction( - sid=order.sid, + asset=order.asset, amount=int(amount), dt=dt, price=price, diff --git a/zipline/protocol.py b/zipline/protocol.py index 8013251e..fd78a710 100644 --- a/zipline/protocol.py +++ b/zipline/protocol.py @@ -16,6 +16,8 @@ from warnings import warn import pandas as pd +from zipline.assets import Asset +from zipline.utils.input_validation import expect_types from .utils.enum import enum from zipline._protocol import BarData # noqa @@ -225,14 +227,17 @@ class Account(object): class Position(object): - - def __init__(self, sid): - self.sid = sid + @expect_types(asset=Asset) + def __init__(self, asset): + self.asset = asset self.amount = 0 self.cost_basis = 0.0 # per share self.last_sale_price = 0.0 self.last_sale_date = None + # for backwards compatibility + self.sid = asset + def __repr__(self): return "Position({0})".format(self.__dict__) diff --git a/zipline/utils/factory.py b/zipline/utils/factory.py index 805213f6..a529ed06 100644 --- a/zipline/utils/factory.py +++ b/zipline/utils/factory.py @@ -21,6 +21,8 @@ import pandas as pd import numpy as np from datetime import timedelta, datetime +from zipline.assets import Asset +from zipline.finance.transaction import Transaction from zipline.protocol import Event, DATASOURCE_TYPE from zipline.sources import SpecificEquityTrades from zipline.finance.trading import SimulationParameters @@ -30,7 +32,7 @@ from zipline.data.loader import ( # For backwards compatibility load_bars_from_yahoo, ) from zipline.utils.calendars import get_calendar - +from zipline.utils.input_validation import expect_types __all__ = ['load_from_yahoo', 'load_bars_from_yahoo'] @@ -150,27 +152,27 @@ def create_split(sid, ratio, date): }) -def create_txn(sid, price, amount, datetime): - txn = Event({ - 'sid': sid, - 'amount': amount, - 'dt': datetime, - 'price': price, - 'type': DATASOURCE_TYPE.TRANSACTION, - 'source_id': 'MockTransactionSource' - }) - return txn +@expect_types(asset=Asset) +def create_txn(asset, price, amount, datetime, order_id): + return Transaction( + asset=asset, + price=price, + amount=amount, + dt=datetime, + order_id=order_id, + ) -def create_txn_history(sid, priceList, amtList, interval, sim_params, +@expect_types(asset=Asset) +def create_txn_history(asset, priceList, amtList, interval, sim_params, trading_calendar): txns = [] current = sim_params.first_open for price, amount in zip(priceList, amtList): - current = get_next_trading_dt(current, interval, trading_calendar) + dt = get_next_trading_dt(current, interval, trading_calendar) - txns.append(create_txn(sid, price, amount, current)) + txns.append(create_txn(asset, price, amount, dt, None)) current = current + interval return txns From 450690801a45f107ad2ab70a815cb8335b27f002 Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Wed, 19 Apr 2017 21:18:08 -0400 Subject: [PATCH 2/9] BUG: Position cost basis was calculated incorrectly for Futures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For futures, we need to divide the position’s commission by the contract size to get a per-unit commission in order to properly update the position’s cost basis. --- tests/test_perf_tracking.py | 35 +++++++++++++++++++++++++ zipline/finance/performance/position.py | 6 ++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index f0ac8af6..94acc3d0 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -2056,6 +2056,41 @@ class TestPositionTracker(WithTradingEnvironment, self.assertEqual(100 + 200 + 300000 + 400000, pos_stats.gross_exposure) self.assertEqual(100 - 200 + 300000 - 400000, pos_stats.net_exposure) + def test_cost_basis(self): + dt = pd.Timestamp("2015-12-10 15:00", tz='UTC') + + equity_pos = perf.Position( + self.EQUITY1, + amount=10, + last_sale_date=dt, + cost_basis=10, + last_sale_price=11, + ) + + future_pos = perf.Position( + self.FUTURE3, + amount=10, + last_sale_date=dt, + cost_basis=10, + last_sale_price=11, + ) + + self.assertEqual(10, equity_pos.cost_basis) + + # send a $5 commission to the equity position. Spread out over 10 + # shares, that bumps the cost basis by $0.50. + equity_pos.adjust_commission_cost_basis(self.EQUITY1, 5) + self.assertEqual(10.5, equity_pos.cost_basis) + + self.assertEqual(10, future_pos.cost_basis) + + # send a $5k commission to the futures position. since self.FUTURE3 + # has a contract size (multipler) of 1000, this should result in a + # $10.5 updated cost basis. (5000 / 1000 = $5, spread out over 10 + # contracts, is $0.50 extra per contract). + future_pos.adjust_commission_cost_basis(self.FUTURE3, 5000) + self.assertEqual(10.5, future_pos.cost_basis) + def test_update_positions(self): pt = perf.PositionTracker(self.env.asset_finder, None) dt = pd.Timestamp("2014/01/01 3:00PM") diff --git a/zipline/finance/performance/position.py b/zipline/finance/performance/position.py index 24bc5bb4..a73ee913 100644 --- a/zipline/finance/performance/position.py +++ b/zipline/finance/performance/position.py @@ -173,7 +173,11 @@ class Position(object): return prev_cost = self.cost_basis * self.amount - new_cost = prev_cost + cost + if isinstance(asset, Future): + cost_to_use = cost / asset.multiplier + else: + cost_to_use = cost + new_cost = prev_cost + cost_to_use self.cost_basis = new_cost / self.amount def __repr__(self): From 483012ccf6ee3a8b161d4d3a924bbf72bc5a6b19 Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Fri, 21 Apr 2017 14:00:07 -0400 Subject: [PATCH 3/9] REF: Make dataportal emit splits that hold Assets, not sids --- zipline/data/data_portal.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/zipline/data/data_portal.py b/zipline/data/data_portal.py index 39250ef1..09860cfc 100644 --- a/zipline/data/data_portal.py +++ b/zipline/data/data_portal.py @@ -1121,24 +1121,24 @@ class DataPortal(object): self._asset_end_dates[sid] = asset.end_date - def get_splits(self, sids, dt): + def get_splits(self, assets, dt): """ Returns any splits for the given sids and the given dt. Parameters ---------- - sids : container - Sids for which we want splits. + assets : container + Assets for which we want splits. dt : pd.Timestamp The date for which we are checking for splits. Note: this is expected to be midnight UTC. Returns ------- - splits : list[(int, float)] - List of splits, where each split is a (sid, ratio) tuple. + splits : list[(asset, float)] + List of splits, where each split is a (asset, ratio) tuple. """ - if self._adjustment_reader is None or not sids: + if self._adjustment_reader is None or not assets: return {} # convert dt to # of seconds since epoch, because that's what we use @@ -1149,7 +1149,9 @@ class DataPortal(object): "SELECT sid, ratio FROM SPLITS WHERE effective_date = ?", (seconds,)).fetchall() - splits = [split for split in splits if split[0] in sids] + splits = [split for split in splits if split[0] in assets] + splits = [(self.asset_finder.retrieve_asset(split[0]), split[1]) + for split in splits] return splits From b7b8c46d74c9e0a2b6a64933784fe82b05cb960b Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Fri, 21 Apr 2017 14:00:21 -0400 Subject: [PATCH 4/9] REF: Blotter no longer needs AssetFinder --- tests/finance/test_blotter.py | 28 +++++++++------------------- tests/test_algorithm.py | 9 +++------ tests/test_finance.py | 14 +++++--------- zipline/algorithm.py | 1 - zipline/finance/blotter.py | 4 +--- zipline/testing/core.py | 6 ++---- 6 files changed, 20 insertions(+), 42 deletions(-) diff --git a/tests/finance/test_blotter.py b/tests/finance/test_blotter.py index adfc5eee..95786f7f 100644 --- a/tests/finance/test_blotter.py +++ b/tests/finance/test_blotter.py @@ -151,8 +151,7 @@ class BlotterTestCase(WithCreateBarData, self.assertEqual(list(blotter.open_orders), [self.asset_25]) def test_blotter_eod_cancellation(self): - blotter = Blotter('minute', self.asset_finder, - cancel_policy=EODCancel()) + blotter = Blotter('minute', cancel_policy=EODCancel()) # Make two orders for the same asset, so we can test that we are not # mutating the orders list as we are cancelling orders @@ -175,8 +174,7 @@ class BlotterTestCase(WithCreateBarData, self.assertEqual(order.status, ORDER_STATUS.CANCELLED) def test_blotter_never_cancel(self): - blotter = Blotter('minute', self.asset_finder, - cancel_policy=NeverCancel()) + blotter = Blotter('minute', cancel_policy=NeverCancel()) blotter.order(self.asset_24, 100, MarketOrder()) @@ -190,8 +188,7 @@ class BlotterTestCase(WithCreateBarData, self.assertEqual(blotter.new_orders[0].status, ORDER_STATUS.OPEN) def test_order_rejection(self): - blotter = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter = Blotter(self.sim_params.data_frequency) # Reject a nonexistent order -> no order appears in new_order, # no exceptions raised out @@ -220,8 +217,7 @@ class BlotterTestCase(WithCreateBarData, # Do it again, but reject it at a later time (after tradesimulation # pulls it from new_orders) - blotter = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter = Blotter(self.sim_params.data_frequency) new_open_id = blotter.order(self.asset_24, 10, MarketOrder()) new_open_order = blotter.open_orders[self.asset_24][0] self.assertEqual(new_open_id, new_open_order.id) @@ -237,8 +233,7 @@ class BlotterTestCase(WithCreateBarData, # You can't reject a filled order. # Reset for paranoia - blotter = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter = Blotter(self.sim_params.data_frequency) blotter.slippage_models[Equity] = FixedSlippage() filled_id = blotter.order(self.asset_24, 100, MarketOrder()) filled_order = None @@ -303,8 +298,7 @@ class BlotterTestCase(WithCreateBarData, expected_status = ORDER_STATUS.OPEN if expected_open else \ ORDER_STATUS.FILLED - blotter = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter = Blotter(self.sim_params.data_frequency) open_id = blotter.order(self.asset_24, order_size, MarketOrder()) open_order = blotter.open_orders[self.asset_24][0] self.assertEqual(open_id, open_order.id) @@ -326,8 +320,7 @@ class BlotterTestCase(WithCreateBarData, self.assertEqual(filled_order.open_amount, expected_open) def test_prune_orders(self): - blotter = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter = Blotter(self.sim_params.data_frequency) blotter.order(self.asset_24, 100, MarketOrder()) open_order = blotter.open_orders[self.asset_24][0] @@ -354,10 +347,8 @@ class BlotterTestCase(WithCreateBarData, Ensure the effect of order_batch is the same as multiple calls to order. """ - blotter1 = Blotter(self.sim_params.data_frequency, - self.asset_finder) - blotter2 = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter1 = Blotter(self.sim_params.data_frequency) + blotter2 = Blotter(self.sim_params.data_frequency) for i in range(1, 4): order_arg_lists = [ (self.asset_24, i * 100, MarketOrder()), @@ -386,7 +377,6 @@ class BlotterTestCase(WithCreateBarData, def test_slippage_and_commission_dispatching(self): blotter = Blotter( self.sim_params.data_frequency, - self.asset_finder, equity_slippage=FixedSlippage(spread=0.0), future_slippage=FixedSlippage(spread=2.0), equity_commission=PerTrade(cost=1.0), diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index cb98d12a..e44b14b9 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -1790,8 +1790,7 @@ def handle_data(context, data): def test_batch_order_target_percent_matches_multi_order(self): weights = pd.Series([.3, .7]) - multi_blotter = RecordBatchBlotter(self.SIM_PARAMS_DATA_FREQUENCY, - self.asset_finder) + multi_blotter = RecordBatchBlotter(self.SIM_PARAMS_DATA_FREQUENCY) multi_test_algo = TradingAlgorithm( script=dedent("""\ from collections import OrderedDict @@ -1820,8 +1819,7 @@ def handle_data(context, data): multi_stats = multi_test_algo.run(self.data_portal) self.assertFalse(multi_blotter.order_batch_called) - batch_blotter = RecordBatchBlotter(self.SIM_PARAMS_DATA_FREQUENCY, - self.asset_finder) + batch_blotter = RecordBatchBlotter(self.SIM_PARAMS_DATA_FREQUENCY) batch_test_algo = TradingAlgorithm( script=dedent("""\ from collections import OrderedDict @@ -1864,8 +1862,7 @@ def handle_data(context, data): def test_batch_order_target_percent_filters_null_orders(self): weights = pd.Series([1, 0]) - batch_blotter = RecordBatchBlotter(self.SIM_PARAMS_DATA_FREQUENCY, - self.asset_finder) + batch_blotter = RecordBatchBlotter(self.SIM_PARAMS_DATA_FREQUENCY) batch_test_algo = TradingAlgorithm( script=dedent("""\ from collections import OrderedDict diff --git a/tests/test_finance.py b/tests/test_finance.py index f176d608..4145ea92 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -275,8 +275,7 @@ class FinanceTestCase(WithLogger, else: slippage_func = None - blotter = Blotter(sim_params.data_frequency, self.env.asset_finder, - slippage_func) + blotter = Blotter(sim_params.data_frequency, slippage_func) start_date = sim_params.first_open @@ -366,22 +365,19 @@ class FinanceTestCase(WithLogger, ) def test_blotter_processes_splits(self): - blotter = Blotter('daily', self.env.asset_finder, - equity_slippage=FixedSlippage()) + blotter = Blotter('daily', equity_slippage=FixedSlippage()) # set up two open limit orders with very low limit prices, # one for sid 1 and one for sid 2 - blotter.order( - blotter.asset_finder.retrieve_asset(1), 100, LimitOrder(10)) - blotter.order( - blotter.asset_finder.retrieve_asset(2), 100, LimitOrder(10)) + blotter.order(self.asset_finder.retrieve_asset(1), 100, LimitOrder(10)) + blotter.order(self.asset_finder.retrieve_asset(2), 100, LimitOrder(10)) # send in a split for sid 2 blotter.process_splits([(2, 0.3333)]) for sid in [1, 2]: order_lists = \ - blotter.open_orders[blotter.asset_finder.retrieve_asset(sid)] + blotter.open_orders[self.asset_finder.retrieve_asset(sid)] self.assertIsNotNone(order_lists) self.assertEqual(1, len(order_lists)) diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 476dc706..8ec1883b 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -322,7 +322,6 @@ class TradingAlgorithm(object): if not self.blotter: self.blotter = Blotter( data_frequency=self.data_frequency, - asset_finder=self.asset_finder, # Default to NeverCancel in zipline cancel_policy=self.cancel_policy, ) diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index 9ca934b6..d27721ee 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -34,7 +34,7 @@ warning_logger = Logger('AlgoWarning') class Blotter(object): - def __init__(self, data_frequency, asset_finder, equity_slippage=None, + def __init__(self, data_frequency, equity_slippage=None, future_slippage=None, equity_commission=None, future_commission=None, cancel_policy=None): # these orders are aggregated by asset @@ -43,8 +43,6 @@ class Blotter(object): # keep a dict of orders by their own id self.orders = {} - self.asset_finder = asset_finder - # holding orders that have come in since the last event. self.new_orders = [] self.current_dt = None diff --git a/zipline/testing/core.py b/zipline/testing/core.py index abfe110b..47eee138 100644 --- a/zipline/testing/core.py +++ b/zipline/testing/core.py @@ -1512,10 +1512,8 @@ def ensure_doctest(f, name=None): class RecordBatchBlotter(Blotter): """Blotter that tracks how its batch_order method was called. """ - def __init__(self, data_frequency, asset_finder): - super(RecordBatchBlotter, self).__init__( - data_frequency, asset_finder, - ) + def __init__(self, data_frequency): + super(RecordBatchBlotter, self).__init__(data_frequency) self.order_batch_called = [] def batch_order(self, *args, **kwargs): From 0b4b058065620a0bffc21ae48c3fa47426f0fb21 Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Sat, 22 Apr 2017 14:01:57 -0400 Subject: [PATCH 5/9] REF: Remove asset_finder and multipliers from PositionTracker --- tests/finance/test_blotter.py | 7 +- tests/test_perf_tracking.py | 147 +++++++++--------- zipline/errors.py | 12 -- .../finance/performance/position_tracker.py | 111 ++++--------- zipline/finance/performance/tracker.py | 1 - 5 files changed, 103 insertions(+), 175 deletions(-) diff --git a/tests/finance/test_blotter.py b/tests/finance/test_blotter.py index 95786f7f..aaaa2bf7 100644 --- a/tests/finance/test_blotter.py +++ b/tests/finance/test_blotter.py @@ -108,7 +108,7 @@ class BlotterTestCase(WithCreateBarData, (StopLimitOrder(10, 20), 10, 20)]) def test_blotter_order_types(self, style_obj, expected_lmt, expected_stp): - blotter = Blotter('daily', self.asset_finder) + blotter = Blotter('daily') blotter.order(self.asset_24, 100, style_obj) result = blotter.open_orders[self.asset_24][0] @@ -117,7 +117,7 @@ class BlotterTestCase(WithCreateBarData, self.assertEqual(result.stop, expected_stp) def test_cancel(self): - blotter = Blotter('daily', self.asset_finder) + blotter = Blotter('daily') oid_1 = blotter.order(self.asset_24, 100, MarketOrder()) oid_2 = blotter.order(self.asset_24, 200, MarketOrder()) @@ -261,8 +261,7 @@ class BlotterTestCase(WithCreateBarData, status indication. When a fill happens, the order should switch status to OPEN/FILLED as necessary """ - blotter = Blotter(self.sim_params.data_frequency, - self.asset_finder) + blotter = Blotter(self.sim_params.data_frequency) # Nothing happens on held of a non-existent order blotter.hold(56) self.assertEqual(blotter.new_orders, []) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 94acc3d0..23214016 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -276,6 +276,7 @@ class TestSplitPerformance(WithSimParams, WithTmpDir, ZiplineTestCase): def init_class_fixtures(cls): super(TestSplitPerformance, cls).init_class_fixtures() cls.asset1 = cls.env.asset_finder.retrieve_asset(1) + cls.asset2 = cls.env.asset_finder.retrieve_asset(2) def test_multiple_splits(self): # if multiple positions all have splits at the same time, verify that @@ -284,17 +285,14 @@ class TestSplitPerformance(WithSimParams, WithTmpDir, ZiplineTestCase): self.trading_calendar, self.env) - asset1 = self.asset_finder.retrieve_asset(1) - asset2 = self.asset_finder.retrieve_asset(2) - perf_tracker.position_tracker.positions[1] = \ - Position(asset1, amount=10, cost_basis=10, last_sale_price=11) + Position(self.asset1, amount=10, cost_basis=10, last_sale_price=11) perf_tracker.position_tracker.positions[2] = \ - Position(asset2, amount=10, cost_basis=10, last_sale_price=11) + Position(self.asset2, amount=10, cost_basis=10, last_sale_price=11) leftover_cash = perf_tracker.position_tracker.handle_splits( - [(1, 0.333), (2, 0.333)] + [(self.asset1, 0.333), (self.asset2, 0.333)] ) # we used to have 10 shares that each cost us $10, total $100 @@ -329,7 +327,7 @@ class TestSplitPerformance(WithSimParams, WithTmpDir, ZiplineTestCase): # set up a split with ratio 3 occurring at the start of the second # day. splits = { - events[1].dt: [(1, 3)] + events[1].dt: [(self.asset1, 3)] } results = calculate_results(self.sim_params, @@ -1106,8 +1104,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, txn1 = create_txn(self.asset1, trades_1[0].dt, 10.0, 100) txn2 = create_txn(self.asset2, trades_1[0].dt, 10.0, -100) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency) pp.position_tracker = pt @@ -1199,8 +1196,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, self.sim_params, {1: trades}) txn = create_txn(self.asset1, trades[1].dt, 10.0, 1000) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency) pp.position_tracker = pt @@ -1291,8 +1287,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, self.sim_params, {1: trades}) txn = create_txn(self.asset1, trades[1].dt, 10.0, 100) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency, period_open=self.sim_params.start_session, @@ -1410,8 +1405,7 @@ single short-sale transaction""" {1: trades}) txn = create_txn(self.asset1, trades[1].dt, 10.0, -100) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod( 1000.0, self.env.asset_finder, self.sim_params.data_frequency) @@ -1530,8 +1524,7 @@ single short-sale transaction""" ) # now run a performance period encompassing the entire trade sample. - ptTotal = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + ptTotal = perf.PositionTracker(self.sim_params.data_frequency) ppTotal = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency) ppTotal.position_tracker = pt @@ -1638,8 +1631,7 @@ trade after cover""" short_txn = create_txn(self.asset1, trades[1].dt, 10.0, -100) cover_txn = create_txn(self.asset1, trades[6].dt, 7.0, 100) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency) pp.position_tracker = pt @@ -1725,8 +1717,7 @@ shares in position" self.sim_params, {1: trades}) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod( 1000.0, self.env.asset_finder, @@ -1792,8 +1783,7 @@ shares in position" self.assertEqual(pp.pnl, -800, "this period goes from +400 to -400") - pt3 = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt3 = perf.PositionTracker(self.sim_params.data_frequency) pp3 = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency) pp3.position_tracker = pt3 @@ -1845,8 +1835,7 @@ shares in position" transactions = factory.create_txn_history(*history_args) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency) pp.position_tracker = pt @@ -1885,8 +1874,7 @@ shares in position" self.sim_params, {1: trades}) txn = create_txn(self.asset1, trades[0].dt, 10.0, 100) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency, period_open=self.sim_params.start_session, @@ -1929,8 +1917,7 @@ shares in position" self.sim_params, {1: trades}) txn = create_txn(self.asset1, trades[0].dt, 10.0, 100) - pt = perf.PositionTracker(self.env.asset_finder, - self.sim_params.data_frequency) + pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, self.sim_params.data_frequency, period_open=self.sim_params.start_session, @@ -2003,8 +1990,7 @@ class TestPositionTracker(WithTradingEnvironment, """ sim_params = factory.create_simulation_parameters(num_days=4) - pt = perf.PositionTracker(self.env.asset_finder, - sim_params.data_frequency) + pt = perf.PositionTracker(sim_params.data_frequency) pos_stats = pt.stats() stats = [ @@ -2025,17 +2011,27 @@ class TestPositionTracker(WithTradingEnvironment, self.assertNotIsInstance(val, (bool, np.bool_)) def test_position_values_and_exposures(self): - pt = perf.PositionTracker(self.env.asset_finder, None) + pt = perf.PositionTracker(None) dt = pd.Timestamp("1984/03/06 3:00PM") - pos1 = perf.Position(self.EQUITY1, amount=np.float64(10.0), - last_sale_date=dt, last_sale_price=10) - pos2 = perf.Position(self.EQUITY2, amount=np.float64(-20.0), - last_sale_date=dt, last_sale_price=10) - pos3 = perf.Position(self.FUTURE3, amount=np.float64(30.0), - last_sale_date=dt, last_sale_price=10) - pos4 = perf.Position(self.FUTURE4, amount=np.float64(-40.0), - last_sale_date=dt, last_sale_price=10) - pt.update_positions({1: pos1, 2: pos2, 3: pos3, 4: pos4}) + pt.update_position( + self.EQUITY1, amount=np.float64(10.0), + last_sale_date=dt, last_sale_price=10 + ) + + pt.update_position( + self.EQUITY2, amount=np.float64(-20.0), + last_sale_date=dt, last_sale_price=10 + ) + + pt.update_position( + self.FUTURE3, amount=np.float64(30.0), + last_sale_date=dt, last_sale_price=10 + ) + + pt.update_position( + self.FUTURE4, amount=np.float64(-40.0), + last_sale_date=dt, last_sale_price=10 + ) # Test long-only methods pos_stats = pt.stats() @@ -2092,24 +2088,35 @@ class TestPositionTracker(WithTradingEnvironment, self.assertEqual(10.5, future_pos.cost_basis) def test_update_positions(self): - pt = perf.PositionTracker(self.env.asset_finder, None) + pt = perf.PositionTracker(None) dt = pd.Timestamp("2014/01/01 3:00PM") - pos1 = perf.Position(self.EQUITY1, amount=np.float64(10.0), - last_sale_date=dt, last_sale_price=10) - pos2 = perf.Position(self.EQUITY2, amount=np.float64(-20.0), - last_sale_date=dt, last_sale_price=10) - pos3 = perf.Position(self.FUTURE5, amount=np.float64(30.0), - last_sale_date=dt, last_sale_price=100) + # pos1 = perf.Position(self.EQUITY1, amount=np.float64(10.0), + # last_sale_date=dt, last_sale_price=10) + # pos2 = perf.Position(self.EQUITY2, amount=np.float64(-20.0), + # last_sale_date=dt, last_sale_price=10) + # pos3 = perf.Position(self.FUTURE5, amount=np.float64(30.0), + # last_sale_date=dt, last_sale_price=100) - # Call update_positions twice. When the second call is made, - # self.positions will already contain data. The order of this data - # needs to be preserved so that it is consistent with the order of the - # data stored in the multipliers OrderedDict()'s. If self.positions - # were to be stored as a dict, then its order could change in arbitrary - # ways when the second update_positions call is made. Hence we also - # store it as an OrderedDict. - pt.update_positions({self.EQUITY1: pos1, self.FUTURE5: pos3}) - pt.update_positions({self.EQUITY2: pos2}) + pt.update_position( + self.EQUITY1, + amount=np.float64(10.0), + last_sale_price=10, + last_sale_date=dt + ) + + pt.update_position( + self.EQUITY2, + amount=np.float64(-20.0), + last_sale_price=10, + last_sale_date=dt + ) + + pt.update_position( + self.FUTURE5, + amount=np.float64(30.0), + last_sale_price=100, + last_sale_date=dt + ) pos_stats = pt.stats() # Test long-only methods @@ -2132,28 +2139,18 @@ class TestPositionTracker(WithTradingEnvironment, self.assertEqual(100 + 150000 - 200, pos_stats.net_exposure) def test_close_position(self): - pt = perf.PositionTracker(self.env.asset_finder, None) + pt = perf.PositionTracker(None) dt = pd.Timestamp('2017/01/04 3:00PM') - pos1 = perf.Position( - asset=self.FUTURE5, - amount=np.float64(30.0), - last_sale_date=dt, - last_sale_price=100, - ) - pos2 = perf.Position( - asset=self.EQUITY1, - amount=np.float64(10.0), - last_sale_date=dt, - last_sale_price=10, + pt.update_position( + asset=self.FUTURE5, amount=np.float64(30.0), + last_sale_date=dt, last_sale_price=100 ) - # Update the positions dictionary with `future_sid` first. The order - # matters because it affects the multipliers dictionaries, which are - # OrderedDicts. If `future_sid` is not removed from the multipliers - # dictionaries, equities will hit the incorrect multiplier when - # computing `pt.stats()`. - pt.update_positions({self.FUTURE5: pos1, self.EQUITY1: pos2}) + pt.update_position( + asset=self.EQUITY1, amount=np.float64(10.0), + last_sale_date=dt, last_sale_price=10 + ) txn = create_txn(self.FUTURE5, dt, 100, -30) pt.execute_transaction(txn) diff --git a/zipline/errors.py b/zipline/errors.py index dfd0a294..04080510 100644 --- a/zipline/errors.py +++ b/zipline/errors.py @@ -665,18 +665,6 @@ class UnsupportedDatetimeFormat(ZiplineError): "coercible to a pandas.Timestamp object.") -class PositionTrackerMissingAssetFinder(ZiplineError): - """ - Raised by a PositionTracker if it is asked to update an Asset but does not - have an AssetFinder - """ - msg = ( - "PositionTracker attempted to update its Asset information but does " - "not have an AssetFinder. This may be caused by a failure to properly " - "de-serialize a TradingAlgorithm." - ) - - class AssetDBVersionError(ZiplineError): """ Raised by an AssetDBWriter or AssetFinder if the version number in the diff --git a/zipline/finance/performance/position_tracker.py b/zipline/finance/performance/position_tracker.py index 0f8edc25..dc4f971b 100644 --- a/zipline/finance/performance/position_tracker.py +++ b/zipline/finance/performance/position_tracker.py @@ -20,11 +20,6 @@ import numpy as np from collections import namedtuple from math import isnan -try: - # optional cython based OrderedDict - from cyordereddict import OrderedDict -except ImportError: - from collections import OrderedDict from six import iteritems, itervalues from zipline.finance.performance.position import Position @@ -32,11 +27,9 @@ from zipline.finance.transaction import Transaction from zipline.utils.input_validation import expect_types import zipline.protocol as zp from zipline.assets import ( - Equity, Future, Asset ) -from zipline.errors import PositionTrackerMissingAssetFinder from . position import positiondict log = logbook.Logger('Performance') @@ -55,18 +48,17 @@ PositionStats = namedtuple('PositionStats', 'net_value']) -def calc_position_values(amounts, - last_sale_prices, - value_multipliers): - iter_amount_price_multiplier = zip( - amounts, - last_sale_prices, - itervalues(value_multipliers), - ) - return [ - price * amount * multiplier for - price, amount, multiplier in iter_amount_price_multiplier - ] +def calc_position_values(positions): + values = [] + + for position in positions: + if isinstance(position.asset, Future): + # Futures don't have an inherent position value. + values.append(0) + else: + values.append(position.last_sale_price * position.amount) + + return values def calc_net(values): @@ -74,18 +66,18 @@ def calc_net(values): return sum(values, np.float64()) -def calc_position_exposures(amounts, - last_sale_prices, - exposure_multipliers): - iter_amount_price_multiplier = zip( - amounts, - last_sale_prices, - itervalues(exposure_multipliers), - ) - return [ - price * amount * multiplier for - price, amount, multiplier in iter_amount_price_multiplier - ] +def calc_position_exposures(positions): + exposures = [] + + for position in positions: + exposure = position.amount * position.last_sale_price + + if isinstance(position.asset, Future): + exposure *= position.asset.multiplier + + exposures.append(exposure) + + return exposures def calc_long_value(position_values): @@ -122,44 +114,15 @@ def calc_gross_value(long_value, short_value): class PositionTracker(object): - def __init__(self, asset_finder, data_frequency): - self.asset_finder = asset_finder - + def __init__(self, data_frequency): # asset => position object self.positions = positiondict() - # Arrays for quick calculations of positions value - self._position_value_multipliers = OrderedDict() - self._position_exposure_multipliers = OrderedDict() self._unpaid_dividends = {} self._unpaid_stock_dividends = {} self._positions_store = zp.Positions() self.data_frequency = data_frequency - def _update_asset(self, asset): - try: - self._position_value_multipliers[asset] - self._position_exposure_multipliers[asset] - except KeyError: - # Check if there is an AssetFinder - if self.asset_finder is None: - raise PositionTrackerMissingAssetFinder() - - # Collect the value multipliers from applicable assets - asset = self.asset_finder.retrieve_asset(asset) - if isinstance(asset, Equity): - self._position_value_multipliers[asset] = 1 - self._position_exposure_multipliers[asset] = 1 - if isinstance(asset, Future): - self._position_value_multipliers[asset] = 0 - self._position_exposure_multipliers[asset] = asset.multiplier - - def update_positions(self, positions): - # update positions in batch - self.positions.update(positions) - for asset, pos in iteritems(positions): - self._update_asset(asset) - @expect_types(asset=Asset) def update_position(self, asset, amount=None, last_sale_price=None, last_sale_date=None, cost_basis=None): @@ -171,7 +134,6 @@ class PositionTracker(object): if amount is not None: position.amount = amount - self._update_asset(asset=asset) if last_sale_price is not None: position.last_sale_price = last_sale_price if last_sale_date is not None: @@ -193,11 +155,7 @@ class PositionTracker(object): position.update(txn) if position.amount == 0: - # if this position now has 0 shares, remove it from our internal - # bookkeeping. del self.positions[asset] - del self._position_value_multipliers[asset] - del self._position_exposure_multipliers[asset] try: # if this position exists in our user-facing dictionary, @@ -205,8 +163,6 @@ class PositionTracker(object): del self._positions_store[asset] except KeyError: pass - else: - self._update_asset(asset) @expect_types(asset=Asset) def handle_commission(self, asset, cost): @@ -221,8 +177,7 @@ class PositionTracker(object): Parameters ---------- splits: list - A list of splits. Each split is a tuple of (sid, ratio), where - sid is an integer. + A list of splits. Each split is a tuple of (asset, ratio). Returns ------- @@ -232,14 +187,12 @@ class PositionTracker(object): total_leftover_cash = 0 for split in splits: - sid = split[0] - asset = self.asset_finder.retrieve_asset(sid) + asset = split[0] if asset in self.positions: # Make the position object handle the split. It returns the # leftover cash from a fractional share, if there is any. position = self.positions[asset] leftover_cash = position.handle_split(asset, split[1]) - self._update_asset(split[0]) total_leftover_cash += leftover_cash return total_leftover_cash @@ -316,7 +269,6 @@ class PositionTracker(object): Position(payment_asset) position.amount += share_count - self._update_asset(payment_asset) return net_cash_payment @@ -408,16 +360,9 @@ class PositionTracker(object): amounts.append(pos.amount) last_sale_prices.append(pos.last_sale_price) - position_values = calc_position_values( - amounts, - last_sale_prices, - self._position_value_multipliers - ) - + position_values = calc_position_values(itervalues(self.positions)) position_exposures = calc_position_exposures( - amounts, - last_sale_prices, - self._position_exposure_multipliers + itervalues(self.positions) ) long_value = calc_long_value(position_values) diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index c7db49ee..d9b5b79d 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -98,7 +98,6 @@ class PerformanceTracker(object): self.emission_rate = sim_params.emission_rate self.position_tracker = PositionTracker( - asset_finder=env.asset_finder, data_frequency=self.sim_params.data_frequency ) From fe84ff358275152d47b22ed623bb540c0382b1ce Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Sat, 22 Apr 2017 14:05:44 -0400 Subject: [PATCH 6/9] REF: Remove assetfinder from PerformancePeriod --- tests/test_perf_tracking.py | 31 ++++++++++---------------- zipline/finance/performance/period.py | 6 ++--- zipline/finance/performance/tracker.py | 2 -- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/tests/test_perf_tracking.py b/tests/test_perf_tracking.py index 23214016..53f52367 100644 --- a/tests/test_perf_tracking.py +++ b/tests/test_perf_tracking.py @@ -1105,8 +1105,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, txn2 = create_txn(self.asset2, trades_1[0].dt, 10.0, -100) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency) pp.position_tracker = pt pt.execute_transaction(txn1) pp.handle_execution(txn1) @@ -1197,8 +1196,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, {1: trades}) txn = create_txn(self.asset1, trades[1].dt, 10.0, 1000) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency) pp.position_tracker = pt pt.execute_transaction(txn) @@ -1288,7 +1286,7 @@ class TestPositionPerformance(WithInstanceTmpDir, WithTradingCalendars, {1: trades}) txn = create_txn(self.asset1, trades[1].dt, 10.0, 100) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency, period_open=self.sim_params.start_session, period_close=self.sim_params.end_session) @@ -1406,9 +1404,7 @@ single short-sale transaction""" txn = create_txn(self.asset1, trades[1].dt, 10.0, -100) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod( - 1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency) pp.position_tracker = pt pt.execute_transaction(txn) @@ -1525,8 +1521,9 @@ single short-sale transaction""" # now run a performance period encompassing the entire trade sample. ptTotal = perf.PositionTracker(self.sim_params.data_frequency) - ppTotal = perf.PerformancePeriod(1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + ppTotal = perf.PerformancePeriod( + 1000.0, self.sim_params.data_frequency + ) ppTotal.position_tracker = pt ptTotal.execute_transaction(txn) @@ -1632,8 +1629,7 @@ trade after cover""" short_txn = create_txn(self.asset1, trades[1].dt, 10.0, -100) cover_txn = create_txn(self.asset1, trades[6].dt, 7.0, 100) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency) pp.position_tracker = pt pt.execute_transaction(short_txn) @@ -1720,7 +1716,6 @@ shares in position" pt = perf.PositionTracker(self.sim_params.data_frequency) pp = perf.PerformancePeriod( 1000.0, - self.env.asset_finder, self.sim_params.data_frequency, period_open=self.sim_params.start_session, period_close=self.sim_params.sessions[-1] @@ -1784,8 +1779,7 @@ shares in position" self.assertEqual(pp.pnl, -800, "this period goes from +400 to -400") pt3 = perf.PositionTracker(self.sim_params.data_frequency) - pp3 = perf.PerformancePeriod(1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + pp3 = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency) pp3.position_tracker = pt3 average_cost = 0 @@ -1836,8 +1830,7 @@ shares in position" transactions = factory.create_txn_history(*history_args) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, - self.sim_params.data_frequency) + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency) pp.position_tracker = pt for idx, (txn, cb) in enumerate(zip(transactions, cost_bases)): @@ -1875,7 +1868,7 @@ shares in position" {1: trades}) txn = create_txn(self.asset1, trades[0].dt, 10.0, 100) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency, period_open=self.sim_params.start_session, period_close=self.sim_params.end_session) @@ -1918,7 +1911,7 @@ shares in position" {1: trades}) txn = create_txn(self.asset1, trades[0].dt, 10.0, 100) pt = perf.PositionTracker(self.sim_params.data_frequency) - pp = perf.PerformancePeriod(1000.0, self.env.asset_finder, + pp = perf.PerformancePeriod(1000.0, self.sim_params.data_frequency, period_open=self.sim_params.start_session, period_close=self.sim_params.end_session) diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index 1a0d8d70..1d009dd5 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -139,7 +139,6 @@ class PerformancePeriod(object): def __init__( self, starting_cash, - asset_finder, data_frequency, period_open=None, period_close=None, @@ -148,7 +147,6 @@ class PerformancePeriod(object): serialize_positions=True, name=None): - self.asset_finder = asset_finder self.data_frequency = data_frequency # Start and end of the entire period @@ -362,7 +360,7 @@ class PerformancePeriod(object): def handle_execution(self, txn): self.cash_flow += self._calculate_execution_cash_flow(txn) - asset = self.asset_finder.retrieve_asset(txn.asset) + asset = txn.asset if isinstance(asset, Future): try: old_price = self._payout_last_sale_prices[asset] @@ -394,7 +392,7 @@ class PerformancePeriod(object): try: multiplier = self._execution_cash_flow_multipliers[txn.asset] except KeyError: - asset = self.asset_finder.retrieve_asset(txn.asset) + asset = txn.asset # Futures experience no cash flow on transactions if isinstance(asset, Future): multiplier = 0 diff --git a/zipline/finance/performance/tracker.py b/zipline/finance/performance/tracker.py index d9b5b79d..6546429c 100644 --- a/zipline/finance/performance/tracker.py +++ b/zipline/finance/performance/tracker.py @@ -140,7 +140,6 @@ class PerformanceTracker(object): keep_orders=False, # don't serialize positions for cumulative period serialize_positions=False, - asset_finder=self.asset_finder, name="Cumulative" ) self.cumulative_performance.position_tracker = self.position_tracker @@ -156,7 +155,6 @@ class PerformanceTracker(object): keep_transactions=True, keep_orders=True, serialize_positions=True, - asset_finder=self.asset_finder, name="Daily" ) self.todays_performance.position_tracker = self.position_tracker From 825866948bbb8c2eb7bf6bee038881d646985788 Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Sat, 22 Apr 2017 14:20:15 -0400 Subject: [PATCH 7/9] BUG: get_splits should return empty list, not empty dict --- tests/test_data_portal.py | 4 ++++ zipline/data/data_portal.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_data_portal.py b/tests/test_data_portal.py index 26391f60..8f08d730 100644 --- a/tests/test_data_portal.py +++ b/tests/test_data_portal.py @@ -401,6 +401,10 @@ class DataPortalTestBase(WithDataPortal, "Asset 10000 had a trade on fourth minute, so should " "return that as the last trade on the fifth.") + def test_get_empty_splits(self): + splits = self.data_portal.get_splits([], self.trading_days[2]) + self.assertEqual([], splits) + class TestDataPortal(DataPortalTestBase): DATA_PORTAL_LAST_AVAILABLE_SESSION = None diff --git a/zipline/data/data_portal.py b/zipline/data/data_portal.py index 09860cfc..bd4cb61b 100644 --- a/zipline/data/data_portal.py +++ b/zipline/data/data_portal.py @@ -1139,7 +1139,7 @@ class DataPortal(object): List of splits, where each split is a (asset, ratio) tuple. """ if self._adjustment_reader is None or not assets: - return {} + return [] # convert dt to # of seconds since epoch, because that's what we use # in the adjustments db From 5b8b2f68bc3612f438fea9dfda32dd612cd34fca Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Sat, 22 Apr 2017 14:43:05 -0400 Subject: [PATCH 8/9] BUG: Blotter should process as many splits as it can --- tests/test_finance.py | 33 +++++++++++++++++++-------------- zipline/finance/blotter.py | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/test_finance.py b/tests/test_finance.py index 4145ea92..914cb090 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -369,11 +369,16 @@ class FinanceTestCase(WithLogger, # set up two open limit orders with very low limit prices, # one for sid 1 and one for sid 2 - blotter.order(self.asset_finder.retrieve_asset(1), 100, LimitOrder(10)) - blotter.order(self.asset_finder.retrieve_asset(2), 100, LimitOrder(10)) + asset1 = self.asset_finder.retrieve_asset(1) + asset2 = self.asset_finder.retrieve_asset(2) + asset133 = self.asset_finder.retrieve_asset(133) - # send in a split for sid 2 - blotter.process_splits([(2, 0.3333)]) + blotter.order(asset1, 100, LimitOrder(10)) + blotter.order(asset2, 100, LimitOrder(10)) + + # send in splits for assets 133 and 2. We have no open orders for + # asset 133 so it should be ignored. + blotter.process_splits([(asset133, 0.5), (asset2, 0.3333)]) for sid in [1, 2]: order_lists = \ @@ -381,19 +386,19 @@ class FinanceTestCase(WithLogger, self.assertIsNotNone(order_lists) self.assertEqual(1, len(order_lists)) - aapl_order = blotter.open_orders[1][0] - fls_order = blotter.open_orders[2][0] + asset1_order = blotter.open_orders[1][0] + asset2_order = blotter.open_orders[2][0] - # 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.asset) + # make sure the asset1 order didn't change + self.assertEqual(100, asset1_order.amount) + self.assertEqual(10, asset1_order.limit) + self.assertEqual(1, asset1_order.asset) - # make sure the fls order did change + # make sure the asset2 order did change # to 300 shares at 3.33 - self.assertEqual(300, fls_order.amount) - self.assertEqual(3.33, fls_order.limit) - self.assertEqual(2, fls_order.asset) + self.assertEqual(300, asset2_order.amount) + self.assertEqual(3.33, asset2_order.limit) + self.assertEqual(2, asset2_order.asset) class TradingEnvironmentTestCase(WithLogger, diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index d27721ee..11d58a11 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -311,7 +311,7 @@ class Blotter(object): """ for asset, ratio in splits: if asset not in self.open_orders: - return + continue orders_to_modify = self.open_orders[asset] for order in orders_to_modify: From 7196e1e4986fdf02969dcd9b18305f4626cef30c Mon Sep 17 00:00:00 2001 From: Jean Bredeche Date: Mon, 24 Apr 2017 15:33:52 -0400 Subject: [PATCH 9/9] MAINT: PR feedback. --- tests/test_finance.py | 5 ++-- zipline/finance/order.py | 2 ++ zipline/finance/performance/period.py | 24 ++++--------------- .../finance/performance/position_tracker.py | 7 +++--- zipline/finance/transaction.py | 2 ++ zipline/protocol.py | 4 +++- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/tests/test_finance.py b/tests/test_finance.py index 914cb090..40667326 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -380,9 +380,8 @@ class FinanceTestCase(WithLogger, # asset 133 so it should be ignored. blotter.process_splits([(asset133, 0.5), (asset2, 0.3333)]) - for sid in [1, 2]: - order_lists = \ - blotter.open_orders[self.asset_finder.retrieve_asset(sid)] + for asset in [asset1, asset2]: + order_lists = blotter.open_orders[asset] self.assertIsNotNone(order_lists) self.assertEqual(1, len(order_lists)) diff --git a/zipline/finance/order.py b/zipline/finance/order.py index 91b3edae..47ee24c3 100644 --- a/zipline/finance/order.py +++ b/zipline/finance/order.py @@ -87,8 +87,10 @@ class Order(object): if self.broker_order_id is None: del dct['broker_order_id'] + # Adding 'sid' for backwards compatibility with downstream consumers. dct['sid'] = self.asset dct['status'] = self.status + return dct @property diff --git a/zipline/finance/performance/period.py b/zipline/finance/performance/period.py index 1d009dd5..89ddbfd8 100644 --- a/zipline/finance/performance/period.py +++ b/zipline/finance/performance/period.py @@ -183,10 +183,6 @@ class PerformancePeriod(object): self._account_store = zp.Account() self.serialize_positions = serialize_positions - # This dict contains the known cash flow multipliers for assets and is - # keyed on asset - self._execution_cash_flow_multipliers = {} - _position_tracker = None def initialize(self, starting_cash, starting_value, starting_exposure): @@ -383,25 +379,15 @@ class PerformancePeriod(object): except KeyError: self.processed_transactions[txn.dt] = [txn] - def _calculate_execution_cash_flow(self, txn): + @staticmethod + def _calculate_execution_cash_flow(txn): """ Calculates the cash flow from executing the given transaction """ - # Check if the multiplier is cached. If it is not, look up the asset - # and cache the multiplier. - try: - multiplier = self._execution_cash_flow_multipliers[txn.asset] - except KeyError: - asset = txn.asset - # Futures experience no cash flow on transactions - if isinstance(asset, Future): - multiplier = 0 - else: - multiplier = 1 - self._execution_cash_flow_multipliers[txn.asset] = multiplier + if txn.asset is Future: + return 0.0 - # Calculate and return the cash flow given the multiplier - return -1 * txn.price * txn.amount * multiplier + return -1 * txn.price * txn.amount # backwards compat. TODO: remove? @property diff --git a/zipline/finance/performance/position_tracker.py b/zipline/finance/performance/position_tracker.py index dc4f971b..67e547dc 100644 --- a/zipline/finance/performance/position_tracker.py +++ b/zipline/finance/performance/position_tracker.py @@ -54,7 +54,7 @@ def calc_position_values(positions): for position in positions: if isinstance(position.asset, Future): # Futures don't have an inherent position value. - values.append(0) + values.append(0.0) else: values.append(position.last_sale_price * position.amount) @@ -186,13 +186,12 @@ class PositionTracker(object): """ total_leftover_cash = 0 - for split in splits: - asset = split[0] + for asset, ratio in splits: if asset in self.positions: # Make the position object handle the split. It returns the # leftover cash from a fractional share, if there is any. position = self.positions[asset] - leftover_cash = position.handle_split(asset, split[1]) + leftover_cash = position.handle_split(asset, ratio) total_leftover_cash += leftover_cash return total_leftover_cash diff --git a/zipline/finance/transaction.py b/zipline/finance/transaction.py index 55814238..94b03300 100644 --- a/zipline/finance/transaction.py +++ b/zipline/finance/transaction.py @@ -39,6 +39,8 @@ class Transaction(object): py = copy(self.__dict__) del py['type'] del py['asset'] + + # Adding 'sid' for backwards compatibility with downstrean consumers. py['sid'] = self.asset return py diff --git a/zipline/protocol.py b/zipline/protocol.py index fd78a710..07f814e4 100644 --- a/zipline/protocol.py +++ b/zipline/protocol.py @@ -235,8 +235,10 @@ class Position(object): self.last_sale_price = 0.0 self.last_sale_date = None + @property + def sid(self): # for backwards compatibility - self.sid = asset + return self.asset def __repr__(self): return "Position({0})".format(self.__dict__)