Skip to content

Commit 2546708

Browse files
author
Scott Sanderson
committed
MAINT: Forward-fill fx rates past file end.
If an FX rate query requests a date that's greater than the last date in the fx rate file, forward-fill from the last value in the file rather than raising an error. We do this for a few reasons: 1. We'd like to gracefully handle the possibility of an FX rates file that's older than another input file. 2. Relative to other non-erroring behaviors, forward-filling is the simplest thing to implement. Specifically, it's what the implementation prior to this change would do naturally if there weren't an explicit check to prevent it. 3. For an FX rates file containing prices on a 24/5 calendar, some amount of forward-filling is required to handle any market with a non-weekday date.
1 parent 1abcb34 commit 2546708

File tree

5 files changed

+17
-25
lines changed

5 files changed

+17
-25
lines changed

tests/data/test_fx.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_scalar_lookup(self):
6969
bases = self.FX_RATES_CURRENCIES + [None]
7070
dates = pd.date_range(
7171
self.FX_RATES_START_DATE - pd.Timedelta('1 day'),
72-
self.FX_RATES_END_DATE,
72+
self.FX_RATES_END_DATE + pd.Timedelta('1 day'),
7373
)
7474
cases = itertools.product(rates, quotes, bases, dates)
7575

@@ -98,7 +98,7 @@ def test_2d_lookup(self):
9898

9999
dates = pd.date_range(
100100
self.FX_RATES_START_DATE - pd.Timedelta('2 days'),
101-
self.FX_RATES_END_DATE
101+
self.FX_RATES_END_DATE + pd.Timedelta('2 days'),
102102
)
103103
rates = self.FX_RATES_RATE_NAMES + [DEFAULT_FX_RATE]
104104
possible_quotes = self.FX_RATES_CURRENCIES
@@ -131,7 +131,7 @@ def test_columnar_lookup(self):
131131

132132
dates = pd.date_range(
133133
self.FX_RATES_START_DATE - pd.Timedelta('2 days'),
134-
self.FX_RATES_END_DATE,
134+
self.FX_RATES_END_DATE + pd.Timedelta('2 days'),
135135
)
136136
rates = self.FX_RATES_RATE_NAMES + [DEFAULT_FX_RATE]
137137
possible_quotes = self.FX_RATES_CURRENCIES
@@ -204,6 +204,7 @@ def test_read_before_start_date(self):
204204
quote = 'USD'
205205
bases = np.array(['CAD'], dtype=object)
206206
dts = pd.DatetimeIndex([bad_date])
207+
207208
result = self.reader.get_rates(rate, quote, bases, dts)
208209
assert_equal(result.shape, (1, 1))
209210
assert_equal(np.nan, result[0, 0])
@@ -221,11 +222,15 @@ def test_read_after_end_date(self):
221222
bases = np.array(['CAD'], dtype=object)
222223
dts = pd.DatetimeIndex([bad_date])
223224

224-
with self.assertRaises(ValueError):
225-
self.reader.get_rates(rate, quote, bases, dts)
226-
227-
with self.assertRaises(ValueError):
228-
self.reader.get_rates_columnar(rate, quote, bases, dts)
225+
result = self.reader.get_rates(rate, quote, bases, dts)
226+
assert_equal(result.shape, (1, 1))
227+
expected = self.get_expected_fx_rate_scalar(
228+
rate,
229+
quote,
230+
'CAD',
231+
self.FX_RATES_END_DATE,
232+
)
233+
assert_equal(expected, result[0, 0])
229234

230235
def test_read_unknown_base(self):
231236
for rate in self.FX_RATES_RATE_NAMES:

zipline/data/fx/hdf5.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def get_rates(self, rate, quote, bases, dts):
190190
if rate == DEFAULT_FX_RATE:
191191
rate = self._default_rate
192192

193-
check_dts(self.dts, dts)
193+
check_dts(dts)
194194

195195
row_ixs = self.dts.searchsorted(dts, side='right') - 1
196196
col_ixs = self.currencies.get_indexer(bases)

zipline/data/fx/in_memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_rates(self, rate, quote, bases, dts):
3636

3737
df = self._data[rate][quote]
3838

39-
check_dts(df.index, dts)
39+
check_dts(dts)
4040

4141
# Get raw values out of the frame.
4242
#

zipline/data/fx/utils.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
11
import numpy as np
22

33

4-
def check_dts(stored_dts, requested_dts):
4+
def check_dts(requested_dts):
5+
"""Validate that ``requested_dts`` are valid for querying from an FX reader.
56
"""
6-
Validate that ``requested_dts`` are valid for querying from an FX reader
7-
that has data for ``stored_dts``.
8-
"""
9-
request_end = requested_dts[-1]
10-
data_end = stored_dts[-1]
11-
127
if not is_sorted_ascending(requested_dts):
138
raise ValueError("Requested fx rates with non-ascending dts.")
149

15-
if request_end > data_end:
16-
raise ValueError(
17-
"Requested fx rates ending at {}, but data ends at {}"
18-
.format(request_end, data_end)
19-
)
20-
2110

2211
def is_sorted_ascending(array):
2312
return (np.maximum.accumulate(array) <= array).all()

zipline/testing/fixtures.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2205,8 +2205,6 @@ def get_expected_fx_rate_scalar(cls, rate, quote, base, dt):
22052205
col = cls.fx_rates[rate][quote][base]
22062206
if dt < col.index[0]:
22072207
return np.nan
2208-
elif dt > col.index[-1]:
2209-
raise ValueError("dt={} > max dt={}".format(dt, col.index[-1]))
22102208

22112209
# PERF: We call this function a lot in some suites, and get_loc is
22122210
# surprisingly expensive, so optimizing it has a meaningful impact on

0 commit comments

Comments
 (0)