ENH: Add adjusted history for continuous futures.

Add `.adj('mul')` and `.adj('add')` methods on ContinuousFuture, which
when used with `history`, will calculate and apply adjustments so that
the values are adjusted to account for discounts and premiums during
rolls.

Example usage in an algo:

```
from zipline.api import continuous_future

def initialize(context):
    context.cl_add = continuous_future('CL', offset=0, roll='calendar').adj('add')
    context.cl_mul = continuous_future('CL', offset=0, roll='calendar').adj('mul')
    context.cl = continuous_future('CL', offset=0, roll='calendar')
    schedule_function(print_history)

def print_history(context, data):
    frame = data.history([context.cl, context.cl_add, context.cl_mul],
                         ['price', 'sid'],
                         20,
                         '1d')
    print 'unadjusted'
    print frame.loc[:, :, context.cl]
    print 'adjusted add'
    print frame.loc[:, :, context.cl_add]
    print 'adjusted mul'
    print frame.loc[:, :, context.cl_mul]
```
This commit is contained in:
Eddie Hebert
2016-10-21 10:18:12 -04:00
parent bf7fdd9685
commit ce37ea64a9
6 changed files with 408 additions and 25 deletions
+189
View File
@@ -483,6 +483,132 @@ def record_current_contract(algo, data):
135441.440,
err_msg="On session after roll, Should be FOJ16's 44th value.")
def test_history_close_session_adjusted(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
cf_mul = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar').adj('mul')
cf_add = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar').adj('add')
window = self.data_portal.get_history_window(
[cf, cf_mul, cf_add],
Timestamp('2016-03-06', tz='UTC'), 30, '1d', 'close')
# Unadjusted value is: 115011.44
# Adjustment is based on hop from 115231.44 to 122240.001
# a ratio of ~1.06
assert_almost_equal(
window.loc['2016-01-26', cf_mul],
122006.62,
err_msg="At beginning of window, should be FOG16's first value, "
"adjusted.")
# Difference of 7008.561
assert_almost_equal(
window.loc['2016-01-26', cf_add],
122020.001,
err_msg="At beginning of window, should be FOG16's first value, "
"adjusted.")
assert_almost_equal(
window.loc['2016-02-26', cf_mul],
125241.440,
err_msg="On session with roll, should be FOH16's 24th value, "
"unadjusted.")
assert_almost_equal(
window.loc['2016-02-26', cf_add],
125241.440,
err_msg="On session with roll, should be FOH16's 24th value, "
"unadjusted.")
assert_almost_equal(
window.loc['2016-02-29', cf_mul],
125251.440,
err_msg="After roll, Should be FOH16's 25th value, unadjusted.")
assert_almost_equal(
window.loc['2016-02-29', cf_add],
125251.440,
err_msg="After roll, Should be FOH16's 25th value, unadjusted.")
# Advance the window a month.
window = self.data_portal.get_history_window(
[cf, cf_mul, cf_add],
Timestamp('2016-04-06', tz='UTC'), 30, '1d', 'close')
# Unadjusted value: 115221.44
# Adjustments based on hops:
# 2016-02-25 00:00:00+00:00 115231.440
# 2016-02-26 00:00:00+00:00 122240.001
# ratio: ~1.061
# difference: 7008.561
# and
# 2016-03-23 00:00:00+00:00 125421.440
# 2016-03-24 00:00:00+00:00 132430.001
# ratio: ~1.056
# difference: 7008.56
assert_almost_equal(
window.loc['2016-02-24', cf_mul],
129059.581,
err_msg="At beginning of window, should be FOG16's 22nd value, "
"with two adjustments.")
assert_almost_equal(
window.loc['2016-02-24', cf_add],
129238.561,
err_msg="At beginning of window, should be FOG16's 22nd value, "
"with two adjustments")
# Unadjusted: 125241.44
assert_almost_equal(
window.loc['2016-02-26', cf_mul],
132239.942,
err_msg="On session with roll, should be FOH16's 24th value, "
"with one adjustment.")
assert_almost_equal(
window.loc['2016-02-26', cf_add],
132250.0,
err_msg="On session with roll, should be FOH16's 24th value, "
"with one adjustment.")
# Unadjusted: 125251.44
assert_almost_equal(
window.loc['2016-02-29', cf_mul],
132250.500,
err_msg="On session after roll, should be FOH16's 25th value, "
"with one adjustment.")
assert_almost_equal(
window.loc['2016-02-29', cf_add],
132260.000,
err_msg="On session after roll, should be FOH16's 25th value, "
"unadjusted.")
# Unadjusted: 135431.44
assert_almost_equal(
window.loc['2016-03-24', cf_mul],
135431.44,
err_msg="On session with roll, should be FOJ16's 43rd value, "
"unadjusted.")
assert_almost_equal(
window.loc['2016-03-24', cf_add],
135431.44,
err_msg="On session with roll, should be FOJ16's 43rd value.")
# Unadjusted: 135441.44
assert_almost_equal(
window.loc['2016-03-28', cf_mul],
135441.44,
err_msg="On session after roll, Should be FOJ16's 44th value.")
assert_almost_equal(
window.loc['2016-03-28', cf_add],
135441.44,
err_msg="On session after roll, Should be FOJ16's 44th value.")
def test_history_close_minute(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
@@ -518,6 +644,69 @@ def record_current_contract(algo, data):
125250.001,
"Should remain FOH16 on next session.")
def test_history_close_minute_adjusted(self):
cf = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar')
cf_mul = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar').adj('mul')
cf_add = self.data_portal.asset_finder.create_continuous_future(
'FO', 0, 'calendar').adj('add')
window = self.data_portal.get_history_window(
[cf, cf_mul, cf_add],
Timestamp('2016-02-25 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1m', 'close')
# Unadjusted: 115231.412
# Adjustment based on roll:
# 2016-02-25 23:00:00+00:00 115231.440
# 2016-02-25 23:01:00+00:00 122240.001
# Ratio: ~1.061
# Difference: 7008.561
self.assertEqual(window.loc['2016-02-25 22:32', cf_mul],
122239.971,
"Should be FOG16 at beginning of window. A minute "
"which is in the 02-25 session, before the roll.")
self.assertEqual(window.loc['2016-02-25 22:32', cf_add],
122239.973,
"Should be FOG16 at beginning of window. A minute "
"which is in the 02-25 session, before the roll.")
# Unadjusted: 115231.44
# Should use same ratios as above.
self.assertEqual(window.loc['2016-02-25 23:00', cf_mul],
122240.001,
"Should be FOG16 on on minute before roll minute, "
"adjusted.")
self.assertEqual(window.loc['2016-02-25 23:00', cf_add],
122240.001,
"Should be FOG16 on on minute before roll minute, "
"adjusted.")
self.assertEqual(window.loc['2016-02-25 23:01', cf_mul],
125240.001,
"Should be FOH16 on minute after roll, unadjusted.")
self.assertEqual(window.loc['2016-02-25 23:01', cf_add],
125240.001,
"Should be FOH16 on minute after roll, unadjusted.")
# Advance the window a session.
window = self.data_portal.get_history_window(
[cf, cf_mul, cf_add],
Timestamp('2016-02-28 18:01', tz='US/Eastern').tz_convert('UTC'),
30, '1m', 'close')
# No adjustments in this window.
self.assertEqual(window.loc['2016-02-26 22:32', cf_mul],
125241.412,
"Should be FOH16 at beginning of window.")
self.assertEqual(window.loc['2016-02-28 23:01', cf_mul],
125250.001,
"Should remain FOH16 on next session.")
class OrderedContractsTestCase(ZiplineTestCase):