Skip to content

Commit b0b20b0

Browse files
author
Scott Sanderson
authored
Merge pull request #2613 from quantopian/fix-only-currency-converted-prices
BUG: Fix crash in pipeline with all currency-converted data.
2 parents 16c66a4 + 8a361e7 commit b0b20b0

File tree

4 files changed

+88
-6
lines changed

4 files changed

+88
-6
lines changed

tests/data/test_fx.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def test_scalar_lookup(self):
8484
expected = self.get_expected_fx_rate_scalar(rate, quote, base, dt)
8585
assert_equal(result_scalar, expected)
8686

87+
alt_result_scalar = reader.get_rate_scalar(rate, quote, base, dt)
88+
assert_equal(result_scalar, alt_result_scalar)
89+
8790
def test_vectorized_lookup(self):
8891
rand = np.random.RandomState(42)
8992

tests/pipeline/test_international_markets.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,51 @@ def test_currency_convert_prices(self, name, domain, calendar_name):
333333

334334
assert_equal(result_2d, expected_result_2d)
335335

336+
@parameterized.expand([
337+
('US', US_EQUITIES, 'XNYS'),
338+
('CA', CA_EQUITIES, 'XTSE'),
339+
('GB', GB_EQUITIES, 'XLON'),
340+
])
341+
def test_only_currency_converted_data(self, name, domain, calendar_name):
342+
# Test running a pipeline on a domain whose assets are all denominated
343+
# in the same currency.
344+
pipe = Pipeline({
345+
'close_USD': EquityPricing.close.fx('USD').latest,
346+
'close_EUR': EquityPricing.close.fx('EUR').latest,
347+
}, domain=domain)
348+
349+
start, end = self.daily_bar_sessions[calendar_name][-2:]
350+
result = self.run_pipeline(pipe, start, end)
351+
352+
calendar = get_calendar(calendar_name)
353+
daily_bars = self.daily_bar_data[calendar_name]
354+
currency_codes = self.daily_bar_currency_codes[calendar_name]
355+
356+
for (dt, asset), row in result.iterrows():
357+
# Subtract a day b/c pipeline output on day N should have prior
358+
# day's price.
359+
price_date = dt - calendar.day
360+
expected_close = daily_bars[asset].loc[price_date, 'close']
361+
expected_base = currency_codes.loc[asset]
362+
363+
expected_rate_USD = self.in_memory_fx_rate_reader.get_rate_scalar(
364+
rate='mid',
365+
quote='USD',
366+
base=expected_base,
367+
dt=price_date.asm8,
368+
)
369+
expected_price = expected_close * expected_rate_USD
370+
assert_equal(row.close_USD, expected_price)
371+
372+
expected_rate_EUR = self.in_memory_fx_rate_reader.get_rate_scalar(
373+
rate='mid',
374+
quote='EUR',
375+
base=expected_base,
376+
dt=price_date.asm8,
377+
)
378+
expected_price = expected_close * expected_rate_EUR
379+
assert_equal(row.close_EUR, expected_price)
380+
336381
def test_explicit_specialization_matches_implicit(self):
337382
pipeline_specialized = Pipeline({
338383
'open': EquityPricing.open.latest,

zipline/data/fx/base.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
from interface import Interface
1+
from interface import default, Interface
2+
3+
import numpy as np
4+
import pandas as pd
25

36
from zipline.utils.sentinel import sentinel
47

@@ -19,13 +22,13 @@ def get_rates(self, rate, quote, bases, dts):
1922
will be used by default for Pipeline API terms that don't specify a
2023
specific rate.
2124
quote : str
22-
Currency code of the currency into to convert.
25+
Currency code of the currency to convert into.
2326
bases : np.array[object]
24-
Array of codes of the currencies from which to convert. A single
25-
currency may appear multiple times.
27+
Array of codes of the currencies to convert from. A single currency
28+
may appear multiple times.
2629
dts : pd.DatetimeIndex
2730
Datetimes for which to load rates. Must be sorted in ascending
28-
order.
31+
order and localized to UTC.
2932
3033
Returns
3134
-------
@@ -36,3 +39,34 @@ def get_rates(self, rate, quote, bases, dts):
3639
The row at index i corresponds to the dt in dts[i].
3740
The column at index j corresponds to the base currency in bases[j].
3841
"""
42+
43+
@default
44+
def get_rate_scalar(self, rate, quote, base, dt):
45+
"""Scalar version of ``get_rates``.
46+
47+
Parameters
48+
----------
49+
rate : str
50+
Rate type to load. Readers intended for use with the Pipeline API
51+
should support at least ``zipline.data.fx.DEFAULT_FX_RATE``, which
52+
will be used by default for Pipeline API terms that don't specify a
53+
specific rate.
54+
quote : str
55+
Currency code of the currency to convert into.
56+
base : str
57+
Currency code of the currency to convert from.
58+
dt : np.datetime64 or pd.Timestamp
59+
Datetime on which to load rate.
60+
61+
Returns
62+
-------
63+
rate : np.float64
64+
Exchange rate from base -> quote on dt.
65+
"""
66+
rates_array = self.get_rates(
67+
rate,
68+
quote,
69+
bases=np.array([base], dtype=object),
70+
dts=pd.DatetimeIndex([dt], tz='UTC'),
71+
)
72+
return rates_array[0, 0]

zipline/pipeline/loaders/equity_pricing_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def _inplace_currency_convert(self, columns, arrays, dates, sids):
166166
by_spec[column.currency_conversion].append(array)
167167

168168
# Nothing to do for terms with no currency conversion.
169-
by_spec.pop(None)
169+
by_spec.pop(None, None)
170170
if not by_spec:
171171
return
172172

0 commit comments

Comments
 (0)