Skip to content

Commit 3ca9ede

Browse files
author
Scott Sanderson
committed
MAINT: Add helper for multi-exchange equity info.
1 parent 7803038 commit 3ca9ede

File tree

4 files changed

+104
-39
lines changed

4 files changed

+104
-39
lines changed

tests/pipeline/test_engine.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -910,11 +910,12 @@ class SyntheticBcolzTestCase(zf.WithAdjustmentReader,
910910
first_asset_start = Timestamp('2015-04-01', tz='UTC')
911911
START_DATE = Timestamp('2015-01-01', tz='utc')
912912
END_DATE = Timestamp('2015-08-01', tz='utc')
913+
ASSET_FINDER_EQUITY_SIDS = list(range(6))
913914

914915
@classmethod
915916
def make_equity_info(cls):
916917
cls.equity_info = ret = make_rotating_equity_info(
917-
num_assets=6,
918+
sids=cls.ASSET_FINDER_EQUITY_SIDS,
918919
first_start=cls.first_asset_start,
919920
frequency=cls.trading_calendar.day,
920921
periods_between_starts=4,

tests/pipeline/test_international_markets.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import numpy as np
44
import pandas as pd
55

6-
from trading_calendars import get_calendar
7-
8-
from zipline.assets.synthetic import make_rotating_equity_info
6+
from zipline.assets.synthetic import (
7+
make_rotating_equity_info,
8+
make_multi_exchange_equity_info,
9+
)
910
from zipline.data.in_memory_daily_bars import InMemoryDailyBarReader
1011
from zipline.pipeline.domain import (
1112
CA_EQUITIES,
@@ -18,7 +19,10 @@
1819
from zipline.pipeline.loaders.equity_pricing_loader import EquityPricingLoader
1920
from zipline.pipeline.loaders.synthetic import NullAdjustmentReader
2021
from zipline.testing.predicates import assert_equal
21-
from zipline.testing.core import parameter_space, random_tick_prices
22+
from zipline.testing.core import (
23+
parameter_space,
24+
random_tick_prices,
25+
)
2226

2327
import zipline.testing.fixtures as zf
2428

@@ -146,29 +150,26 @@ class InternationalEquityTestCase(WithInternationalPricingPipelineEngine,
146150

147151
@classmethod
148152
def make_equity_info(cls):
149-
out = pd.concat(
150-
[
151-
# 15 assets on each exchange. Each asset lives for 5 days.
152-
# A new asset starts each day.
153-
make_rotating_equity_info(
154-
num_assets=20,
155-
first_start=cls.START_DATE,
156-
frequency=get_calendar(exchange).day,
157-
periods_between_starts=1,
158-
# NOTE: The asset_lifetime parameter name is a bit
159-
# misleading. It determines the number of trading
160-
# days between each asset's start_date and end_date,
161-
# so assets created with this method actual "live"
162-
# for (asset_lifetime + 1) days. But, since pipeline
163-
# doesn't show you an asset the day it IPOs, this
164-
# number matches the number of days that each asset
165-
# should appear in a pipeline output.
166-
asset_lifetime=5,
167-
exchange=exchange,
168-
)
169-
for exchange in cls.EXCHANGE_INFO.exchange
170-
],
171-
ignore_index=True,
153+
# - 20 assets on each exchange.
154+
# - Each asset lives for 5 days.
155+
# - A new asset starts each day.
156+
out = make_multi_exchange_equity_info(
157+
factory=make_rotating_equity_info,
158+
exchange_sids={
159+
'XNYS': range(20),
160+
'XTSE': range(20, 40),
161+
'XLON': range(40, 60),
162+
},
163+
first_start=cls.START_DATE,
164+
periods_between_starts=1,
165+
# NOTE: The asset_lifetime parameter name is a bit misleading. It
166+
# determines the number of trading days between each asset's
167+
# start_date and end_date, so assets created with this method
168+
# actual "live" for (asset_lifetime + 1) days. But, since
169+
# pipeline doesn't show you an asset the day it IPOs, this
170+
# number matches the number of days that each asset should
171+
# appear in a pipeline output.
172+
asset_lifetime=5,
172173
)
173174
assert_equal(out.end_date.max(), cls.END_DATE)
174175
return out
@@ -211,7 +212,7 @@ def test_generic_pipeline_with_explicit_domain(self, domain):
211212
expected_dates = sessions[-17:-9]
212213

213214
for col in pipe.columns:
214-
# result_date should look like this:
215+
# result_data should look like this:
215216
#
216217
# E F G H I J K L M N O P # noqa
217218
# 24.17 25.17 26.17 27.17 28.17 NaN NaN NaN NaN NaN NaN NaN # noqa

tests/test_assets.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,27 +1056,30 @@ def test_compute_lifetimes(self):
10561056
equities = pd.concat(
10571057
[
10581058
make_rotating_equity_info(
1059-
num_assets=assets_per_exchange,
1059+
sids=range(
1060+
i * assets_per_exchange,
1061+
(i + 1) * assets_per_exchange,
1062+
),
10601063
first_start=first_start,
10611064
frequency=trading_day,
10621065
periods_between_starts=3,
10631066
asset_lifetime=5,
10641067
exchange=exchange,
10651068
)
1066-
for exchange in (
1069+
for i, exchange in enumerate((
10671070
'US_EXCHANGE_1',
10681071
'US_EXCHANGE_2',
10691072
'CA_EXCHANGE',
10701073
'JP_EXCHANGE',
1071-
)
1074+
))
10721075
],
10731076
ignore_index=True,
10741077
)
10751078
# make every symbol unique
10761079
equities['symbol'] = list(string.ascii_uppercase[:len(equities)])
10771080

10781081
# shuffle up the sids so they are not contiguous per exchange
1079-
sids = np.arange(len(equities))
1082+
sids = equities.index.values[:]
10801083
np.random.RandomState(1337).shuffle(sids)
10811084
equities.index = sids
10821085
permute_sid = dict(zip(sids, range(len(sids)))).__getitem__

zipline/assets/synthetic.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import pandas as pd
55
from pandas.tseries.offsets import MonthBegin
66
from six import iteritems
7+
from toolz import merge
8+
from trading_calendars import get_calendar
79

810
from .futures import CMES_CODE_TO_MONTH
911

1012

11-
def make_rotating_equity_info(num_assets,
13+
def make_rotating_equity_info(sids,
1214
first_start,
1315
frequency,
1416
periods_between_starts,
@@ -38,6 +40,7 @@ def make_rotating_equity_info(num_assets,
3840
info : pd.DataFrame
3941
DataFrame representing newly-created assets.
4042
"""
43+
num_assets = len(sids)
4144
return pd.DataFrame(
4245
{
4346
'symbol': [chr(ord('A') + i) for i in range(num_assets)],
@@ -55,7 +58,7 @@ def make_rotating_equity_info(num_assets,
5558
),
5659
'exchange': exchange,
5760
},
58-
index=range(num_assets),
61+
index=sids,
5962
)
6063

6164

@@ -117,12 +120,13 @@ def make_simple_equity_info(sids,
117120
)
118121

119122

120-
def make_jagged_equity_info(num_assets,
123+
def make_jagged_equity_info(sids,
121124
start_date,
122125
first_end,
123126
frequency,
124127
periods_between_ends,
125-
auto_close_delta):
128+
auto_close_delta,
129+
exchange='TEST'):
126130
"""
127131
Create a DataFrame representing assets that all begin at the same start
128132
date, but have cascading end dates.
@@ -146,6 +150,7 @@ def make_jagged_equity_info(num_assets,
146150
info : pd.DataFrame
147151
DataFrame representing newly-created assets.
148152
"""
153+
num_assets = len(sids)
149154
frame = pd.DataFrame(
150155
{
151156
'symbol': [chr(ord('A') + i) for i in range(num_assets)],
@@ -155,9 +160,9 @@ def make_jagged_equity_info(num_assets,
155160
freq=(periods_between_ends * frequency),
156161
periods=num_assets,
157162
),
158-
'exchange': 'TEST',
163+
'exchange': exchange,
159164
},
160-
index=range(num_assets),
165+
index=sids,
161166
)
162167

163168
# Explicitly pass None to disable setting the auto_close_date column.
@@ -167,6 +172,61 @@ def make_jagged_equity_info(num_assets,
167172
return frame
168173

169174

175+
def make_multi_exchange_equity_info(factory,
176+
exchange_sids,
177+
exchange_kwargs=None,
178+
**common_kwargs):
179+
"""
180+
Create an "equity_info" DataFrame for multiple exchanges by calling an
181+
existing factory function for each exchange and concatting the results.
182+
183+
Parameters
184+
----------
185+
factory : function
186+
Function to use to create equity info for each exchange.
187+
exchange_sids : dict[str -> list[sids]]
188+
Map from exchange to list of sids to be created for that exchange.
189+
exchange_kwargs : dict[str -> dict], optional
190+
Map from exchange to additional kwargs to be passed for that exchange.
191+
**common_kwargs
192+
Additional keyword-arguments are forwarded to ``factory``.
193+
194+
Returns
195+
-------
196+
info : pd.DataFrame
197+
DataFrame representing newly-created assets.
198+
"""
199+
if exchange_kwargs is None:
200+
exchange_kwargs = {e: {} for e in exchange_sids}
201+
else:
202+
assert exchange_kwargs.keys() == exchange_sids.keys()
203+
204+
# When using frequency-based factories, use each calendar's trading
205+
# calendar for frequency by default.
206+
provide_default_frequency = (
207+
'frequency' not in common_kwargs
208+
and factory in (make_rotating_equity_info, make_jagged_equity_info)
209+
)
210+
if provide_default_frequency:
211+
for e, kw in iteritems(exchange_kwargs):
212+
kw.setdefault('frequency', get_calendar(e).day)
213+
214+
frame_per_exchange = [
215+
factory(
216+
sids=sids,
217+
exchange=e,
218+
**merge(common_kwargs, exchange_kwargs[e])
219+
)
220+
for e, sids in iteritems(exchange_sids)
221+
]
222+
223+
result = pd.concat(frame_per_exchange)
224+
if not result.index.is_unique:
225+
raise AssertionError("Duplicate sids: {}".format(result.index))
226+
227+
return result
228+
229+
170230
def make_future_info(first_sid,
171231
root_symbols,
172232
years,

0 commit comments

Comments
 (0)