Skip to content

Commit

Permalink
Merge pull request #2613 from quantopian/fix-only-currency-converted-…
Browse files Browse the repository at this point in the history
…prices

BUG: Fix crash in pipeline with all currency-converted data.
  • Loading branch information
Scott Sanderson committed Jan 14, 2020
2 parents 16c66a4 + 8a361e7 commit b0b20b0
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 6 deletions.
3 changes: 3 additions & 0 deletions tests/data/test_fx.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ def test_scalar_lookup(self):
expected = self.get_expected_fx_rate_scalar(rate, quote, base, dt)
assert_equal(result_scalar, expected)

alt_result_scalar = reader.get_rate_scalar(rate, quote, base, dt)
assert_equal(result_scalar, alt_result_scalar)

def test_vectorized_lookup(self):
rand = np.random.RandomState(42)

Expand Down
45 changes: 45 additions & 0 deletions tests/pipeline/test_international_markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,51 @@ def test_currency_convert_prices(self, name, domain, calendar_name):

assert_equal(result_2d, expected_result_2d)

@parameterized.expand([
('US', US_EQUITIES, 'XNYS'),
('CA', CA_EQUITIES, 'XTSE'),
('GB', GB_EQUITIES, 'XLON'),
])
def test_only_currency_converted_data(self, name, domain, calendar_name):
# Test running a pipeline on a domain whose assets are all denominated
# in the same currency.
pipe = Pipeline({
'close_USD': EquityPricing.close.fx('USD').latest,
'close_EUR': EquityPricing.close.fx('EUR').latest,
}, domain=domain)

start, end = self.daily_bar_sessions[calendar_name][-2:]
result = self.run_pipeline(pipe, start, end)

calendar = get_calendar(calendar_name)
daily_bars = self.daily_bar_data[calendar_name]
currency_codes = self.daily_bar_currency_codes[calendar_name]

for (dt, asset), row in result.iterrows():
# Subtract a day b/c pipeline output on day N should have prior
# day's price.
price_date = dt - calendar.day
expected_close = daily_bars[asset].loc[price_date, 'close']
expected_base = currency_codes.loc[asset]

expected_rate_USD = self.in_memory_fx_rate_reader.get_rate_scalar(
rate='mid',
quote='USD',
base=expected_base,
dt=price_date.asm8,
)
expected_price = expected_close * expected_rate_USD
assert_equal(row.close_USD, expected_price)

expected_rate_EUR = self.in_memory_fx_rate_reader.get_rate_scalar(
rate='mid',
quote='EUR',
base=expected_base,
dt=price_date.asm8,
)
expected_price = expected_close * expected_rate_EUR
assert_equal(row.close_EUR, expected_price)

def test_explicit_specialization_matches_implicit(self):
pipeline_specialized = Pipeline({
'open': EquityPricing.open.latest,
Expand Down
44 changes: 39 additions & 5 deletions zipline/data/fx/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from interface import Interface
from interface import default, Interface

import numpy as np
import pandas as pd

from zipline.utils.sentinel import sentinel

Expand All @@ -19,13 +22,13 @@ def get_rates(self, rate, quote, bases, dts):
will be used by default for Pipeline API terms that don't specify a
specific rate.
quote : str
Currency code of the currency into to convert.
Currency code of the currency to convert into.
bases : np.array[object]
Array of codes of the currencies from which to convert. A single
currency may appear multiple times.
Array of codes of the currencies to convert from. A single currency
may appear multiple times.
dts : pd.DatetimeIndex
Datetimes for which to load rates. Must be sorted in ascending
order.
order and localized to UTC.
Returns
-------
Expand All @@ -36,3 +39,34 @@ def get_rates(self, rate, quote, bases, dts):
The row at index i corresponds to the dt in dts[i].
The column at index j corresponds to the base currency in bases[j].
"""

@default
def get_rate_scalar(self, rate, quote, base, dt):
"""Scalar version of ``get_rates``.
Parameters
----------
rate : str
Rate type to load. Readers intended for use with the Pipeline API
should support at least ``zipline.data.fx.DEFAULT_FX_RATE``, which
will be used by default for Pipeline API terms that don't specify a
specific rate.
quote : str
Currency code of the currency to convert into.
base : str
Currency code of the currency to convert from.
dt : np.datetime64 or pd.Timestamp
Datetime on which to load rate.
Returns
-------
rate : np.float64
Exchange rate from base -> quote on dt.
"""
rates_array = self.get_rates(
rate,
quote,
bases=np.array([base], dtype=object),
dts=pd.DatetimeIndex([dt], tz='UTC'),
)
return rates_array[0, 0]
2 changes: 1 addition & 1 deletion zipline/pipeline/loaders/equity_pricing_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def _inplace_currency_convert(self, columns, arrays, dates, sids):
by_spec[column.currency_conversion].append(array)

# Nothing to do for terms with no currency conversion.
by_spec.pop(None)
by_spec.pop(None, None)
if not by_spec:
return

Expand Down

0 comments on commit b0b20b0

Please sign in to comment.