Skip to content

Instantly share code, notes, and snippets.

@drSeeS
Last active September 12, 2024 10:45
Show Gist options
  • Save drSeeS/ebb7c699972216cfa25240e999561fb9 to your computer and use it in GitHub Desktop.
Save drSeeS/ebb7c699972216cfa25240e999561fb9 to your computer and use it in GitHub Desktop.
Class for constructing rolling futures contracts (at this time only using panama backadj. method on fixed calender days before maturity)
import datetime
import pandas as pd
from pandas.tseries.offsets import Day, BDay, DateOffset
import numpy as np
import os
import pickle
class rollingFuturesContract:
def __init__(self, maturityMonths, rollMonthsBeforeMat, day2roll, adjBDay=1):
'''
Class for building rolling futures contracts
Class obbject carries all instructions for constructing a rolling contract. Actuall construction for a specific symbol is done in get method.
Rolling always takes place on early morning of the roll date.
: maturityMonths : list of futures maturing moths -> [3,6,9,12]
: rollMonthsBeforeMat : how many months before a contract matures the rolling should take place -> 2
: day2roll : on which calender day of a month we should roll -> 15
: adjBDay : if day2roll is on weekend, move it to next Monday (adjBDay>0 or adjBDay=True), or to previous Friday (adjBDay<0)
: returns : new rollingFuturesContract object
'''
self.path = os.path.abspath(__file__)
self.dir_path = os.path.dirname(self.path)
self.params = self.getParameters()
self.stichDayMaxOffset = self.params['stichDayMaxOffset'] # max number of days before the roll date, on which presence of price data will be checked for stitching contracts together
self.rollingTSPath = self.params['rollingTSPath']
self.day2roll = day2roll
self.adjBDay = adjBDay
self.maturityMonths = maturityMonths
self.maturityMonths.sort()
self.rollMonthsList = [self.monthDiff(y, -rollMonthsBeforeMat) for y in self.maturityMonths] # list with months in which rolling takes place
self.maturityMonthsReordered = self.maturityMonths[1:] + self.maturityMonths[:1]
self.rollMonths = pd.DataFrame({'from' : self.maturityMonths, 'into' : self.maturityMonthsReordered}, index=self.rollMonthsList) # df with details of rolling cycle (general info, independent of actual start and end)
self.rollMonths.index.name='month'
self.rollMatrix =[] # df with actual roll details (roll dates, stitch dates, prices, price adjustment etc.)
self.symbol = None
self.datasource = None
self.start = None
self.end = None
self.method = None
self.ts = []
def get(self, symbol, datasource, start, end, method='panama_bk', **kwargs):
'''
Construct actual rolling futures contract for specific symbol
: symbol : instrument symbol -> 'T'
: datasource : source of price data. Currently only 'quandl'. Additional source dependent parameters can be put in **kwargs
: start : start date of time series -> '2000-01-01'
: end : end date of time series -> '2016-12-31'
: method : how rolling contract should be constructed. Currently only 'panama_bk'
: **kwargs : additional parameters required by datasource or method applied (f.e. database name if using datasource='quandl)
'''
self.symbol = symbol
self.datasource = datasource
self.start = start
self.end = end
self.method = method
self.kwargs = kwargs
if method == 'panama_bk':
self.construct_panamabk(symbol, datasource, start, end, **kwargs)
else:
print('could not understand method for constructing the rolling contract: ' + method + '. At the moment you can choose from \'panama_bk\'')
return
return self.ts.copy()
def construct_panamabk(self, symbol, datasource, start, end, **kwargs):
self.datasource = datasource
rm = self.doRollMatrix(start, end)
out=[]
#self.rollMatrix = pd.DataFrame(index=np.arange(0, len(rm)), columns=('from', 'to', 'curContract', 'prevContract', 'nextContract', 'stitchDate', 'unadjCloseCurC', 'adjCloseNextC', 'priceAdjmt', 'priceAdjCum') )
self.rollMatrix = pd.DataFrame(index=np.arange(0, len(rm)), columns=('from', 'to', 'curContract', 'prevContract', 'nextContract', 'stitchDate', 'unadjCloseCurC', 'adjCloseNextC', 'priceAdjmt') )
# treat one contract after the other, coming from the latest to process
p_diff = 0
i=len(rm)-1
while i >= 0:
getDateFrom = rm.ix[i,'from'] - self.stichDayMaxOffset * BDay() # load a little more data so we are sure we can do the stiching
getDateTo = rm.ix[i,'to'] + self.stichDayMaxOffset * BDay()
print('Processing maturity ' + rm.ix[i,'curContract']+': getting raw prices from '+getDateFrom.strftime('%Y-%m-%d')+' to '+getDateTo.strftime('%Y-%m-%d'))
cur = self.getRawPrices(symbol, rm.ix[i,'curContract'], datasource, getDateFrom, getDateTo, **kwargs)
# sticht the contracts together (do stitching only from the second round on)
if i<len(rm)-1:
stitchDate = rm.ix[i+1, 'from'] - DateOffset(days=1) # contract switches in the early morning, price offset is determined from close before rollDate
# if there was not data for the day before the roll date, try the previous stichDayMaxOffset consecutive days (maybe there was a weekend?)
found=False
earliestAllowedStitchDate = stitchDate - self.stichDayMaxOffset*Day()
while stitchDate >= earliestAllowedStitchDate:
try:
p_cur = cur.ix[stitchDate.strftime('%Y-%m-%d'), 'Close']
p_next = next.ix[stitchDate.strftime('%Y-%m-%d'), 'Close']
found=True
#print('Found a stitch date: '+stitchDate.strftime('%Y-%m-%d'))
break
except KeyError:
pass
stitchDate=stitchDate - Day()
if found is False:
raise ValueError('While trying to stich futures contracts: Could not get any overlapping data within allowed perios. Tried backwards until ' + str(self.stichDayMaxOffset) + ' days before roll date')
p_diff = p_next - p_cur # if positive number -> previous series needs to be elevated
#p_diff_cum = p_diff_cum + p_diff
#print('Stitch info: stitch date: ' + stitchDate.strftime('%Y-%m-%d') + ', p_cur: ' + str(p_cur) + ', p_next: ' + str(p_next) + ', p_diff: '+ str(p_diff) + ', p_diff_cum: '+ str(p_diff_cum))
cur['Open'] = cur['Open'].apply(lambda x: x + p_diff)
cur['High'] = cur['High'].apply(lambda x: x + p_diff)
cur['Low'] = cur['Low'].apply(lambda x: x + p_diff)
cur['Close'] = cur['Close'].apply(lambda x: x + p_diff)
#self.rollMatrix.loc[i] = [rm.ix[i,'from'].date(), rm.ix[i,'to'].date(), rm.ix[i,'curContract'], rm.ix[i,'prevContract'], rm.ix[i,'nextContract'], stitchDate.strftime('%Y-%m-%d'), p_cur, p_next, p_diff, p_diff_cum]
self.rollMatrix.loc[i] = [rm.ix[i,'from'].date(), rm.ix[i,'to'].date(), rm.ix[i,'curContract'], rm.ix[i,'prevContract'], rm.ix[i,'nextContract'], stitchDate.strftime('%Y-%m-%d'), p_cur, p_next, p_diff]
else:
# in first round set only 'None' to detail matrix
#self.rollMatrix.iloc[i] = [rm.ix[i,'from'].date(), rm.ix[i,'to'].date(), rm.ix[i,'curContract'], rm.ix[i,'prevContract'], rm.ix[i,'nextContract'], None, None, None, 0, 0]
self.rollMatrix.iloc[i] = [rm.ix[i,'from'].date(), rm.ix[i,'to'].date(), rm.ix[i,'curContract'], rm.ix[i,'prevContract'], rm.ix[i,'nextContract'], None, None, None, 0]
cur['curContract'] = rm.ix[i,'curContract']
cur['priceAdjmt'] = p_diff
out.append(cur[rm.ix[i,'from'].strftime('%Y-%m-%d'):rm.ix[i,'to'].strftime('%Y-%m-%d')])
next=cur
i=i-1
out=list(reversed(out))
self.ts = pd.concat(out)
def doRollMatrix(self, start, end): # create df with roll periods (from, to, curContract, prevContract...)
'''
constructs a df with roll info (current previous and contract, from, to)
'''
rd = pd.to_datetime(start)
l=[rd]
e=pd.to_datetime(end)
while l[-1]<e:
l.append(self.nextRollDate(pd.to_datetime(l[-1])))
del l[-1]
df=pd.DataFrame({'from': l})
#add other columns
df['to']=df['from'].map(lambda x : self.nextRollDate(x) - Day()) # take previous day, not prevBusDay, since some contracts might be trading on the weekend
df['curContract']=df['from'].map(lambda x : self.currentContract(x))
df['prevContract']=df['from'].map(lambda x : self.previousContract(x))
df['nextContract']=df['from'].map(lambda x : self.nextContract(x))
#restrict end of last period to end date (set 'to' to end date)
if df.ix[len(df)-1,'to']>e: df.ix[len(df)-1,'to'] = e
return df
def getRawPrices(self, symbol, maturity, datasource, start, end, **kwargs):
if datasource=='quandl':
from pygruebi.datastore import dsQuandl
q=dsQuandl.dsQuandl(database=kwargs['database'], symbol=symbol, maturity=maturity)
df=q.get(start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d'), **kwargs)
return df
else:
print('could not understand the datasource wanted for constructing the rolling contract: ' + datasource + '. At the moment you can choose from: \'quandl\'')
def monthDiff(self, month, diff): #calculates an offset of diff months to a given month number
o=month+diff
while o<=0: o+=12
return o
def rollDate_adjBDay(self, date):
'''
moves potential roll day of the particular year and month (given in 'date') out of a weekend
'''
d = pd.datetime(date.year, date.month, self.day2roll)
if self.adjBDay>0:
d = d - Day() + BDay() #move weekend to Monday
elif self.adjBDay<0:
d = d + Day() - BDay() # move weekend to Friday
return d
def previousRollMonth(self, date):
if date.month in self.rollMonths.index:
if date<self.rollDate_adjBDay(date):
return self.rollMonths.index[self.rollMonths.index.get_loc(date.month)-1]
else:
return date.month
else:
#print('date.month: '+str(date.month))
if date.month>=self.rollMonths.index[0]:
return self.rollMonths.index[self.rollMonths.index.get_loc(date.month, method='pad')]
else:
return self.rollMonths.index[-1]
def nextRollMonth(self, date):
if date.month in self.rollMonths.index:
if date>=self.rollDate_adjBDay(date):
try:
return self.rollMonths.index[self.rollMonths.index.get_loc(date.month)+1]
except IndexError:
return self.rollMonths.index[0]
else:
return date.month
else:
try:
return self.rollMonths.index[self.rollMonths.index.get_loc(date.month, method='bfill')]
except KeyError:
return self.rollMonths.index[0]
def previousRollDate(self, date):
w = self.previousRollMonth(date)
if w > date.month:
d = datetime.date(date.year-1, w, self.day2roll)
return self.rollDate_adjBDay(d)
else:
d = datetime.date(date.year, w, self.day2roll)
return self.rollDate_adjBDay(d)
def nextRollDate(self, date):
w = self.nextRollMonth(date)
if w < date.month:
d = datetime.date(date.year+1, w, self.day2roll)
return self.rollDate_adjBDay(d)
else:
d = datetime.date(date.year, w, self.day2roll)
return self.rollDate_adjBDay(d)
def currentContract(self, date):
w = self.previousRollMonth(date)
c = self.rollMonths.loc[w, 'into']
if c < date.month:
return '%04d%02d' % (date.year+1, c)
else:
return '%04d%02d' % (date.year, c)
def previousContract(self, date):
w = self.previousRollMonth(date)
c = self.rollMonths.loc[w, 'from']
if c < date.month:
return '%04d%02d' % (date.year+1, c)
else:
return '%04d%02d' % (date.year, c)
def nextContract(self, date):
w = self.nextRollMonth(date)
c = self.rollMonths.loc[w, 'into']
if c < date.month:
return '%04d%02d' % (date.year+1, c)
else:
return '%04d%02d' % (date.year, c)
def save(self):
filepath = self.rollingTSPath + self.symbol + '_' + self.datasource + '.cc'
self.ts.to_csv(filepath,sep=';', index=True, date_format='%Y-%m-%dT%H:%M:%S')
def maturitiesPerDay(self, start, end):
'''
returns df with business days, giving for each day the current contract
'''
f=lambda x: self.currentContract(x)
r=pd.date_range(pd.to_datetime(start), pd.to_datetime(end))
r=r[r.dayofweek<5]
df=pd.DataFrame(r, index=r, columns=['maturity']).applymap(f)
return df
def getParameters(self):
with open(self.dir_path+'\\futures_parameters.pickle', 'rb') as f:
return pickle.load(f)
'''
import pickle
d ={'stichDayMaxOffset' : 3
,'rollingTSPath' : 'Z:/Sync/StartMe/Data/_Rolling/'
}
with open('Z:/path/to/package/futures_parameters.pickle', 'wb') as f:
pickle.dump(d, f)
'''
@ChrisAllisonMalta
Copy link

Hi
Just working through this now, it looks good! There are a couple of bugs I've found so far:

  1. Maybe this is covered your Quandl wrapper but the base Quandl returns Settle not "Close" for CME CL or ICE T ?
  2. I seem to get an infinite loop if you only supply one month for a particular contract (line 169 in doRollMatrix)
    while l[-1] < e: l.append(self.nextRollDate(pd.to_datetime(l[-1])))
  3. I guess its important to note that the data frame returned by the Quandl code has the date as the index? Maybe you could post your Quandl wrapper code to ensure compatibility or at least the specification of what it returns?

Thanks Chris

@js190
Copy link

js190 commented Dec 21, 2016

Hi has anyone looked at this recently? Wanted to know if I have the most recent version before testing this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment