Skip to content

Commit c825927

Browse files
author
Scott Sanderson
authored
Merge pull request #2617 from quantopian/columnar-fx-rates
ENH: Add get_rates_columnar method to FXRatesReader.
2 parents b0b20b0 + c6bb976 commit c825927

File tree

3 files changed

+128
-14
lines changed

3 files changed

+128
-14
lines changed

tests/data/test_fx.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,13 @@ 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+
col_result = reader.get_rates_columnar(rate, quote, bases, dts)
88+
assert_equal(col_result, result.ravel())
89+
8790
alt_result_scalar = reader.get_rate_scalar(rate, quote, base, dt)
8891
assert_equal(result_scalar, alt_result_scalar)
8992

90-
def test_vectorized_lookup(self):
93+
def test_2d_lookup(self):
9194
rand = np.random.RandomState(42)
9295

9396
dates = pd.date_range(self.FX_RATES_START_DATE, self.FX_RATES_END_DATE)
@@ -113,6 +116,34 @@ def test_vectorized_lookup(self):
113116

114117
assert_equal(result, expected)
115118

119+
def test_columnar_lookup(self):
120+
rand = np.random.RandomState(42)
121+
122+
dates = pd.date_range(self.FX_RATES_START_DATE, self.FX_RATES_END_DATE)
123+
rates = self.FX_RATES_RATE_NAMES + [DEFAULT_FX_RATE]
124+
currencies = self.FX_RATES_CURRENCIES
125+
reader = self.reader
126+
127+
# For every combination of rate name and quote currency...
128+
for rate, quote in itertools.product(rates, currencies):
129+
for N in 1, 2, 10, 200:
130+
# Choose N (date, base) pairs randomly with replacement.
131+
dts_raw = rand.choice(dates, N, replace=True)
132+
dts = pd.DatetimeIndex(dts_raw, tz='utc').sort_values()
133+
bases = rand.choice(currencies, N, replace=True)
134+
135+
# ... And check that we get the expected result when querying
136+
# for those dates/currencies.
137+
result = reader.get_rates_columnar(rate, quote, bases, dts)
138+
expected = self.get_expected_fx_rates_columnar(
139+
rate,
140+
quote,
141+
bases,
142+
dts,
143+
)
144+
145+
assert_equal(result, expected)
146+
116147
def test_load_everything(self):
117148
# Sanity check for the randomized tests above: check that we get
118149
# exactly the rates we set up in make_fx_rates if we query for their

zipline/data/fx/base.py

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,64 @@
99

1010

1111
class FXRateReader(Interface):
12+
"""
13+
Interface for reading foreign exchange (fx) rates.
14+
15+
An FX rate reader contains one or more distinct "rates", each of which
16+
corresponds to a collection of mappings from (quote, base, dt) ->
17+
float. The value produced for a given (quote, base, dt) triple is the
18+
exchange rate to use when converting from ``base`` to ``quote`` on ``dt``.
19+
20+
The specific set of rates contained in a particular reader is
21+
user-defined. We infer no particular semantics from their names, other than
22+
that they are distinct rates. Examples of possible rate names might be
23+
things like "bid", "mid", and "ask", or "london_close", "tokyo_close",
24+
"nyse_close".
25+
26+
Implementations of :class:`FXRateReader` must provide at least one method::
27+
28+
def get_rates(self, rate, quote, bases, dts):
29+
30+
which takes a rate, a quote currency, an array of base currencies, and an
31+
array of dts, and produces a (len(dts), len(base))-shape array containing a
32+
conversion rates for all pairs in the cartesian product of bases and dts.
33+
34+
Given a definition of :meth:`get_rates`, this interface automatically
35+
generates two additional methods::
36+
37+
def get_rates_scalar(self, rate, quote, base, dt):
38+
39+
and::
40+
41+
def get_rates_columnar(self, rate, quote, bases, dts):
42+
43+
:meth:`get_rates_scalar` takes scalar-valued ``base`` and ``dt`` values,
44+
and returns a scalar float value for the requested fx rate.
45+
46+
:meth:`get_rates_columnar` takes parallel arrays of ``bases`` and ``dts``
47+
and returns a same-length array of fx rates by performing a lookup on the
48+
(base, dt) pairs drawn from zipping together ``bases``, and ``dts``. In
49+
other words, its behavior is equivalent to::
50+
51+
def get_rates_columnnar(self, rate, quote, bases, dts):
52+
out = []
53+
for base, dt in zip(bases, dts):
54+
out.append(self.get_rate_scalar(rate, quote, base, dt))
55+
return np.array(out)
56+
"""
1257

1358
def get_rates(self, rate, quote, bases, dts):
1459
"""
15-
Get rates to convert ``bases`` into ``quote``.
60+
Load a 2D array of fx rates.
1661
1762
Parameters
1863
----------
1964
rate : str
20-
Rate type to load. Readers intended for use with the Pipeline API
21-
should support at least ``zipline.data.fx.DEFAULT_FX_RATE``, which
22-
will be used by default for Pipeline API terms that don't specify a
23-
specific rate.
65+
Name of the rate to load.
2466
quote : str
2567
Currency code of the currency to convert into.
2668
bases : np.array[object]
27-
Array of codes of the currencies to convert from. A single currency
69+
Array of codes of the currencies to convert from. The same currency
2870
may appear multiple times.
2971
dts : pd.DatetimeIndex
3072
Datetimes for which to load rates. Must be sorted in ascending
@@ -42,15 +84,13 @@ def get_rates(self, rate, quote, bases, dts):
4284

4385
@default
4486
def get_rate_scalar(self, rate, quote, base, dt):
45-
"""Scalar version of ``get_rates``.
87+
"""
88+
Load a scalar FX rate value.
4689
4790
Parameters
4891
----------
4992
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.
93+
Name of the rate to load.
5494
quote : str
5595
Currency code of the currency to convert into.
5696
base : str
@@ -63,10 +103,44 @@ def get_rate_scalar(self, rate, quote, base, dt):
63103
rate : np.float64
64104
Exchange rate from base -> quote on dt.
65105
"""
66-
rates_array = self.get_rates(
106+
rates_2d = self.get_rates(
67107
rate,
68108
quote,
69109
bases=np.array([base], dtype=object),
70110
dts=pd.DatetimeIndex([dt], tz='UTC'),
71111
)
72-
return rates_array[0, 0]
112+
return rates_2d[0, 0]
113+
114+
@default
115+
def get_rates_columnar(self, rate, quote, bases, dts):
116+
"""
117+
Load a 1D array of FX rates.
118+
119+
Parameters
120+
----------
121+
rate : str
122+
Name of the rate to load.
123+
quote : str
124+
Currency code of the currency to convert into.
125+
bases : np.array[object]
126+
Array of codes of the currencies to convert from. The same currency
127+
may appear multiple times.
128+
dts : np.DatetimeIndex
129+
Datetimes for which to load rates. The same value may appear
130+
multiple times, but datetimes must be sorted in ascending order and
131+
localized to UTC.
132+
"""
133+
if len(bases) != len(dts):
134+
raise ValueError(
135+
"len(bases) ({}) != len(dts) ({})".format(len(bases), len(dts))
136+
)
137+
138+
unique_bases, bases_ix = np.unique(bases, return_inverse=True)
139+
unique_dts, dts_ix = np.unique(dts.values, return_inverse=True)
140+
rates_2d = self.get_rates(
141+
rate,
142+
quote,
143+
unique_bases,
144+
pd.DatetimeIndex(unique_dts, tz='utc')
145+
)
146+
return rates_2d[dts_ix, bases_ix]

zipline/testing/fixtures.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,6 +2222,15 @@ def get_expected_fx_rates(cls, rate, quote, bases, dts):
22222222

22232223
return out
22242224

2225+
@classmethod
2226+
def get_expected_fx_rates_columnar(cls, rate, quote, bases, dts):
2227+
assert len(bases) == len(dts)
2228+
rates = [
2229+
cls.get_expected_fx_rate_scalar(rate, quote, base, dt)
2230+
for base, dt in zip(bases, dts)
2231+
]
2232+
return np.array(rates, dtype='float64')
2233+
22252234

22262235
def fast_get_loc_ffilled(dts, dt):
22272236
"""

0 commit comments

Comments
 (0)