BUG: Fix bounds errors in roll finder.

Fix common error condition which was triggered whenever the session at the end
of the prefetched history window was a session where the back contract was
active. When the back contract was the active contract, the next contract for
consideration was the front contract at the end of the window, which
definitionally always has an autoclose after the end of the window.
Instead, just start seeking backwards from the end of the window.

Also prevent lookahead bias in volume rolls, which was caused by the using the
volume for a session to determine whether that session had rolled. Information
that would not have been available at the beginning of the session.

This change makes the volume rolls overly conservative, and may be improved by
looking at vectors of the preceding volume and making the roll off of momentum.
This commit is contained in:
Eddie Hebert
2016-11-18 16:38:05 -05:00
parent f3a36fe97f
commit 4d434acab7
2 changed files with 44 additions and 36 deletions
+21 -19
View File
@@ -176,18 +176,20 @@ class ContinuousFuturesTestCase(WithCreateBarData,
vol_stop_session = sid_to_vol_stop_session[i]
m_open = tc.open_and_close_for_session(vol_stop_session)[0]
loc = dts.searchsorted(m_open)
# Add a little bit of noise to roll. So that checks for exacly
# 0 do not work, since there may be stragglers after a roll.
# Add a little bit of noise to roll. So that predicates that
# check for exactly 0 do not work, since there may be
# stragglers after a roll.
df.volume.values[loc] = 1000
df.volume.values[loc + 1:] = 0
j = i - 1
if j in sid_to_vol_stop_session:
non_primary_end = sid_to_vol_stop_session[j] - sessions.freq
non_primary_end = sid_to_vol_stop_session[j]
m_close = tc.open_and_close_for_session(non_primary_end)[1]
loc = dts.searchsorted(m_close)
# Add some volume before a roll, since a contracted may be
# entered earlier than when it is the primary.
df.volume.values[:loc] = 2000
if m_close > dts[0]:
loc = dts.get_loc(m_close)
# Add some volume before a roll, since a contract may be
# entered earlier than when it is the primary.
df.volume.values[:loc + 1] = 10
yield i, df
def test_create_continuous_future(self):
@@ -311,14 +313,14 @@ class ContinuousFuturesTestCase(WithCreateBarData,
lambda: pd.Timestamp('2016-01-26', tz='UTC'))
contract = bar_data.current(cf_primary, 'contract')
self.assertEqual(contract.symbol, 'FOG16')
self.assertEqual(contract.symbol, 'FOF16')
bar_data = self.create_bardata(
lambda: pd.Timestamp('2016-01-26', tz='UTC'))
lambda: pd.Timestamp('2016-01-27', tz='UTC'))
contract = bar_data.current(cf_primary, 'contract')
self.assertEqual(contract.symbol, 'FOG16',
'Auto close at beginning of session. FOG16 remains '
'Auto close at beginning of session. FOG16 is now '
'the current contract.')
bar_data = self.create_bardata(
@@ -599,12 +601,12 @@ def record_current_contract(algo, data):
# Volume cuts out for FOF16 on 2016-01-25
self.assertEqual(window.loc['2016-01-26', cf],
1,
"Should be FOG16 at beginning of window.")
0,
"Should be FOF16 at beginning of window.")
self.assertEqual(window.loc['2016-01-27', cf],
1,
"Should have remained FOG16.")
"Should have rolled to FOG16.")
self.assertEqual(window.loc['2016-02-25', cf],
1,
@@ -630,24 +632,24 @@ def record_current_contract(algo, data):
self.assertEqual(window.loc['2016-02-26', cf],
2,
"Should be FOH16 on session with roll.")
"Should be FOH16 on roll session.")
self.assertEqual(window.loc['2016-02-29', cf],
2,
"Should be FOH16 on session after roll.")
"Should remain FOH16.")
self.assertEqual(window.loc['2016-03-17', cf],
2,
"Should be FOH16 on session before volume cuts out.")
self.assertEqual(window.loc['2016-03-18', cf],
3,
"Should be FOJ16 on session where the volume of "
"FOH16 cuts out.")
2,
"Should be FOH16 on session where the volume of "
"FOH16 cuts out, the roll is upcoming.")
self.assertEqual(window.loc['2016-03-24', cf],
3,
"Should have remained FOJ16.")
"Should have rolled to FOJ16.")
self.assertEqual(window.loc['2016-03-28', cf],
3,
+23 -17
View File
@@ -96,23 +96,21 @@ class RollFinder(with_metaclass(ABCMeta, object)):
i -= 1
else:
i -= 2
auto_close_date = Timestamp(oc.auto_close_dates[i], tz='UTC')
while auto_close_date > start and i > -1:
session_loc = sessions.searchsorted(auto_close_date)
curr = sessions[-1]
while curr > start and i > -1:
session_loc = sessions.searchsorted(curr)
front = oc.contract_sids[i]
back = oc.contract_sids[i + 1]
while session_loc > -1:
while session_loc > 0:
session = sessions[session_loc]
if back != self._active_contract(oc, front, back, session):
prev = sessions[session_loc - 1]
if back != self._active_contract(oc, front, back, prev):
rolls.insert(0, (oc.contract_sids[i + offset], session))
break
session_loc -= 1
roll_session = sessions[session_loc + 1]
if roll_session > start:
rolls.insert(0, (oc.contract_sids[i + offset],
roll_session))
i -= 1
auto_close_date = Timestamp(oc.auto_close_dates[i],
tz='UTC')
curr = Timestamp(oc.auto_close_dates[i],
tz='UTC')
return rolls
@@ -131,8 +129,8 @@ class CalendarRollFinder(RollFinder):
if sid == front:
break
auto_close_date = Timestamp(oc.auto_close_dates[i], tz='UTC')
before_auto_close = dt < auto_close_date
return front if before_auto_close else back
auto_closed = dt >= auto_close_date
return back if auto_closed else front
class VolumeRollFinder(RollFinder):
@@ -149,7 +147,15 @@ class VolumeRollFinder(RollFinder):
self.session_reader = session_reader
def _active_contract(self, oc, front, back, dt):
# FIXME: Possible vector for look ahead bias.
front_vol = self.session_reader.get_value(front, dt, 'volume')
back_vol = self.session_reader.get_value(back, dt, 'volume')
return back if back_vol > front_vol else front
prev = dt - self.trading_calendar.day
front_vol = self.session_reader.get_value(front, prev, 'volume')
back_vol = self.session_reader.get_value(back, prev, 'volume')
if back_vol > front_vol:
return back
else:
for i, sid in enumerate(oc.contract_sids):
if sid == front:
break
auto_close_date = Timestamp(oc.auto_close_dates[i], tz='UTC')
auto_closed = dt >= auto_close_date
return back if auto_closed else front