From 568bf0aa598eb7b4779cfb6f356e6477c03ec578 Mon Sep 17 00:00:00 2001 From: David Michalowicz Date: Tue, 30 May 2017 13:38:22 -0400 Subject: [PATCH 1/2] ENH: Add method for computing current portfolio weights --- tests/test_algorithm.py | 97 +++++++++++++++++++++++++++++++++++++- zipline/protocol.py | 26 +++++++++- zipline/test_algorithms.py | 17 +++++++ 3 files changed, 137 insertions(+), 3 deletions(-) diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index d9198ddc..b124901b 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -123,6 +123,7 @@ from zipline.test_algorithms import ( TestOrderPercentAlgorithm, TestOrderStyleForwardingAlgorithm, TestOrderValueAlgorithm, + TestPositionWeightsAlgorithm, TestRegisterTransformAlgorithm, TestTargetAlgorithm, TestTargetPercentAlgorithm, @@ -1095,10 +1096,62 @@ class TestPositions(WithLogger, START_DATE = pd.Timestamp('2006-01-03', tz='utc') END_DATE = pd.Timestamp('2006-01-06', tz='utc') - sids = ASSET_FINDER_EQUITY_SIDS = [1, 133] + ASSET_FINDER_EQUITY_SIDS = (1, 133) + + @classmethod + def make_equity_daily_bar_data(cls): + frame = pd.DataFrame( + { + 'open': [90, 95, 100, 105], + 'high': [90, 95, 100, 105], + 'low': [90, 95, 100, 105], + 'close': [90, 95, 100, 105], + 'volume': 100, + }, + index=cls.equity_daily_bar_days, + ) + return ((sid, frame) for sid in cls.asset_finder.equities_sids) + + @classmethod + def make_futures_info(cls): + return pd.DataFrame.from_dict( + { + 1000: { + 'symbol': 'CLF06', + 'root_symbol': 'CL', + 'start_date': cls.START_DATE, + 'end_date': cls.END_DATE, + 'auto_close_date': cls.END_DATE + cls.trading_calendar.day, + 'exchange': 'CME', + 'multiplier': 100, + }, + }, + orient='index', + ) + + @classmethod + def make_future_minute_bar_data(cls): + trading_calendar = cls.trading_calendars[Future] + + sids = cls.asset_finder.futures_sids + minutes = trading_calendar.minutes_for_sessions_in_range( + cls.future_minute_bar_days[0], + cls.future_minute_bar_days[-1], + ) + frame = pd.DataFrame( + { + 'open': 2.0, + 'high': 2.0, + 'low': 2.0, + 'close': 2.0, + 'volume': 100, + }, + index=minutes, + ) + return ((sid, frame) for sid in sids) def test_empty_portfolio(self): - algo = EmptyPositionsAlgorithm(self.sids, + algo = EmptyPositionsAlgorithm(self.asset_finder.equities_sids, sim_params=self.sim_params, env=self.env) daily_stats = algo.run(self.data_portal) @@ -1124,6 +1177,46 @@ class TestPositions(WithLogger, empty_positions = daily_stats.positions.map(lambda x: len(x) == 0) self.assertTrue(empty_positions.all()) + def test_position_weights(self): + sids = (1, 133, 1000) + equity_1, equity_133, future_1000 = \ + self.asset_finder.retrieve_all(sids) + + algo = TestPositionWeightsAlgorithm( + sids_and_amounts=zip(sids, [1, -1, 1]), + sim_params=self.sim_params, + env=self.env, + ) + daily_stats = algo.run(self.data_portal) + + expected_position_weights = [ + # No positions held on the first day. + pd.Series({}), + # Each equity's weight is its price times the number of shares + # held. For example, we hold a long position in equity_1 so its + # weight is (+95.0 * 1) = 95. For a futures contract, its weight is + # the unit price times number of shares held times the multiplier. + # For future_1000, this is (2.0 * 1 * 100) = 200. + pd.Series({ + equity_1: 95.0 / (95 + 95 + 200), + equity_133: -95.0 / (95 + 95 + 200), + future_1000: 200.0 / (95 + 95 + 200), + }), + pd.Series({ + equity_1: 100.0 / (100 + 100 + 200), + equity_133: -100.0 / (100 + 100 + 200), + future_1000: 200.0 / (100 + 100 + 200), + }), + pd.Series({ + equity_1: 105.0 / (105 + 105 + 200), + equity_133: -105.0 / (105 + 105 + 200), + future_1000: 200.0 / (105 + 105 + 200), + }), + ] + + for i, expected in enumerate(expected_position_weights): + assert_equal(daily_stats.iloc[i]['position_weights'], expected) + class TestBeforeTradingStart(WithDataPortal, WithSimParams, diff --git a/zipline/protocol.py b/zipline/protocol.py index 5d944347..2287b48e 100644 --- a/zipline/protocol.py +++ b/zipline/protocol.py @@ -16,7 +16,7 @@ from warnings import warn import pandas as pd -from zipline.assets import Asset +from zipline.assets import Asset, Future from zipline.utils.input_validation import expect_types from .utils.enum import enum from zipline._protocol import BarData # noqa @@ -136,6 +136,10 @@ class Order(Event): ) +def asset_multiplier(asset): + return asset.multiplier if isinstance(asset, Future) else 1 + + class Portfolio(object): def __init__(self): @@ -169,6 +173,26 @@ class Portfolio(object): }, ) + @property + def current_portfolio_weights(self): + """ + Compute each asset's weight in the portfolio by calculating its held + value divided by the total value of all positions. + + Each equity's value is its price times the number of shares held. Each + futures contract's value is its unit price times number of shares held + times the multiplier. + """ + position_values = pd.Series({ + asset: ( + position.last_sale_price * + position.amount * + asset_multiplier(asset) + ) + for asset, position in self.positions.items() + }) + return position_values / position_values.abs().sum() + class Account(object): ''' diff --git a/zipline/test_algorithms.py b/zipline/test_algorithms.py index de6e923a..b43162da 100644 --- a/zipline/test_algorithms.py +++ b/zipline/test_algorithms.py @@ -691,6 +691,23 @@ class EmptyPositionsAlgorithm(TradingAlgorithm): self.record(num_positions=len(self.portfolio.positions)) +class TestPositionWeightsAlgorithm(TradingAlgorithm): + """ + An algorithm that records the weights of its portfolio holdings each day. + """ + def initialize(self, sids_and_amounts, *args, **kwargs): + self.ordered = False + self.sids_and_amounts = sids_and_amounts + + def handle_data(self, data): + if not self.ordered: + for s, amount in self.sids_and_amounts: + self.order(self.sid(s), amount) + self.ordered = True + + self.record(position_weights=self.portfolio.current_portfolio_weights) + + class InvalidOrderAlgorithm(TradingAlgorithm): """ An algorithm that tries to make various invalid order calls, verifying that From 602a799a6be4889b8a79f45f737ae27f1b894003 Mon Sep 17 00:00:00 2001 From: David Michalowicz Date: Wed, 7 Jun 2017 15:46:45 -0400 Subject: [PATCH 2/2] Fix weights calculation to use portfolio value as denominator --- tests/test_algorithm.py | 35 ++++++++++++++++++++--------------- zipline/protocol.py | 2 +- zipline/test_algorithms.py | 6 +++++- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index b124901b..e8781d66 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -1095,6 +1095,7 @@ class TestPositions(WithLogger, ZiplineTestCase): START_DATE = pd.Timestamp('2006-01-03', tz='utc') END_DATE = pd.Timestamp('2006-01-06', tz='utc') + SIM_PARAMS_CAPITAL_BASE = 1000 ASSET_FINDER_EQUITY_SIDS = (1, 133) @@ -1183,7 +1184,7 @@ class TestPositions(WithLogger, self.asset_finder.retrieve_all(sids) algo = TestPositionWeightsAlgorithm( - sids_and_amounts=zip(sids, [1, -1, 1]), + sids_and_amounts=zip(sids, [2, -1, 1]), sim_params=self.sim_params, env=self.env, ) @@ -1192,25 +1193,29 @@ class TestPositions(WithLogger, expected_position_weights = [ # No positions held on the first day. pd.Series({}), - # Each equity's weight is its price times the number of shares - # held. For example, we hold a long position in equity_1 so its - # weight is (+95.0 * 1) = 95. For a futures contract, its weight is - # the unit price times number of shares held times the multiplier. - # For future_1000, this is (2.0 * 1 * 100) = 200. + # Each equity's position value is its price times the number of + # shares held. In this example, we hold a long position in 2 shares + # of equity_1 so its weight is (95.0 * 2) = 190.0 divided by the + # total portfolio value. The total portfolio value is the sum of + # cash ($905.00) plus the value of all equity positions. + # + # For a futures contract, its weight is the unit price times number + # of shares held times the multiplier. For future_1000, this is + # (2.0 * 1 * 100) = 200.0 divided by total portfolio value. pd.Series({ - equity_1: 95.0 / (95 + 95 + 200), - equity_133: -95.0 / (95 + 95 + 200), - future_1000: 200.0 / (95 + 95 + 200), + equity_1: 190.0 / (190.0 - 95.0 + 905.0), + equity_133: -95.0 / (190.0 - 95.0 + 905.0), + future_1000: 200.0 / (190.0 - 95.0 + 905.0), }), pd.Series({ - equity_1: 100.0 / (100 + 100 + 200), - equity_133: -100.0 / (100 + 100 + 200), - future_1000: 200.0 / (100 + 100 + 200), + equity_1: 200.0 / (200.0 - 100.0 + 905.0), + equity_133: -100.0 / (200.0 - 100.0 + 905.0), + future_1000: 200.0 / (200.0 - 100.0 + 905.0), }), pd.Series({ - equity_1: 105.0 / (105 + 105 + 200), - equity_133: -105.0 / (105 + 105 + 200), - future_1000: 200.0 / (105 + 105 + 200), + equity_1: 210.0 / (210.0 - 105.0 + 905.0), + equity_133: -105.0 / (210.0 - 105.0 + 905.0), + future_1000: 200.0 / (210.0 - 105.0 + 905.0), }), ] diff --git a/zipline/protocol.py b/zipline/protocol.py index 2287b48e..665d5242 100644 --- a/zipline/protocol.py +++ b/zipline/protocol.py @@ -191,7 +191,7 @@ class Portfolio(object): ) for asset, position in self.positions.items() }) - return position_values / position_values.abs().sum() + return position_values / self.portfolio_value class Account(object): diff --git a/zipline/test_algorithms.py b/zipline/test_algorithms.py index b43162da..e77d5fde 100644 --- a/zipline/test_algorithms.py +++ b/zipline/test_algorithms.py @@ -88,7 +88,7 @@ from zipline.api import ( ) from zipline.errors import UnsupportedOrderParameters from zipline.assets import Future, Equity -from zipline.finance.commission import PerShare +from zipline.finance.commission import PerShare, PerTrade from zipline.finance.execution import ( LimitOrder, MarketOrder, @@ -698,6 +698,10 @@ class TestPositionWeightsAlgorithm(TradingAlgorithm): def initialize(self, sids_and_amounts, *args, **kwargs): self.ordered = False self.sids_and_amounts = sids_and_amounts + self.set_commission(us_equities=PerTrade(0), us_futures=PerTrade(0)) + self.set_slippage( + us_equities=FixedSlippage(0), us_futures=FixedSlippage(0), + ) def handle_data(self, data): if not self.ordered: