Source code for pyqstrat.markets

import re
import datetime
from dateutil._common import weekday
import dateutil.relativedelta as rd
import numpy as np
from pyqstrat.holiday_calendars import Calendar, get_date_from_weekday
from typing import Tuple, Mapping

FUTURE_CODES_INT = {'F': 1, 'G': 2, 'H': 3, 'J': 4, 'K': 5, 'M': 6, 'N': 7, 'Q': 8, 'U': 9, 'V': 10, 'X': 11, 'Z': 12}
FUTURES_CODES_INVERTED: Mapping[int, str] = {v: k for k, v in FUTURE_CODES_INT.items()}

FUTURE_CODES_STR = {'F': 'jan', 'G': 'feb', 'H': 'mar', 'J': 'apr', 'K': 'may', 'M': 'jun', 
                    'N': 'jul', 'Q': 'aug', 'U': 'sep', 'V': 'oct', 'X': 'nov', 'Z': 'dec'}


[docs]def future_code_to_month(future_code: str) -> str: ''' Given a future code such as "X", return the month abbreviation, such as "nov" Args: future_code (str): the one letter future code >>> future_code_to_month('X') 'nov' ''' assert len(future_code) == 1, f'Future code must be a single character: {future_code}' if future_code not in FUTURE_CODES_STR: raise Exception(f'unknown future code: {future_code}') return FUTURE_CODES_STR[future_code] if future_code not in FUTURE_CODES_INT: raise Exception(f'unknown future code: {future_code}') return FUTURE_CODES_INT[future_code]
[docs]def future_code_to_month_number(future_code: str) -> int: ''' Given a future code such as "X", return the month number (from 1 - 12) Args: future_code (str): the one letter future code >>> future_code_to_month_number('X') 11 ''' assert len(future_code) == 1, f'Future code must be a single character: {future_code}' if future_code not in FUTURE_CODES_INT: raise Exception(f'unknown future code: {future_code}') return FUTURE_CODES_INT[future_code]
[docs]def get_future_code(month: int) -> str: ''' Given a month number such as 3 for March, return the future code for it, e.g. H >>> get_future_code(3) 'H' ''' return FUTURES_CODES_INVERTED[month]
[docs]class EminiFuture: calendar = Calendar.get_calendar(Calendar.NYSE)
[docs] @staticmethod def get_current_symbol(curr_date: datetime.date) -> str: ''' >>> assert(EminiFuture.get_current_symbol(datetime.date(2019, 3, 14)) == 'ESH9') >>> assert(EminiFuture.get_current_symbol(datetime.date(2019, 3, 15)) == 'ESM9') ''' year = curr_date.year month = curr_date.month day = curr_date.day third_friday = EminiFuture.calendar.third_friday_of_month(month, year).astype(datetime.date) if month < 3 or (month == 3 and day < third_friday.day): month_str = 'H' elif month < 6 or (month == 6 and day < third_friday.day): month_str = 'M' elif month < 9 or (month == 9 and day < third_friday.day): month_str = 'U' elif month < 12 or (month == 12 and day < third_friday.day): month_str = 'Z' else: month_str = 'H' year += 1 fut_symbol = 'ES' + month_str + str(year - 2010) return fut_symbol
[docs] @staticmethod def get_previous_symbol(curr_future_symbol: str) -> str: ''' >>> assert(EminiFuture.get_previous_symbol('ESH9') == 'ESZ8') ''' month = curr_future_symbol[2] year = int(curr_future_symbol[3]) prev_month = {'H': 'Z', 'M': 'H', 'U': 'M', 'Z': 'U'}[month] prev_year = year if prev_month != 'Z' else year - 1 if prev_year == -1: prev_year == 9 return f'ES{prev_month}{prev_year}'
[docs] @staticmethod def get_next_symbol(curr_future_symbol: str) -> str: ''' >>> assert(EminiFuture.get_next_symbol('ESZ8') == 'ESH9') ''' month = curr_future_symbol[2] year = int(curr_future_symbol[3]) next_month = {'Z': 'H', 'H': 'M', 'M': 'U', 'U': 'Z'}[month] next_year = year if next_month != 'H' else year + 1 if next_year == 10: next_year == 0 return f'ES{next_month}{next_year}'
[docs] @staticmethod def get_expiry(fut_symbol: str) -> np.datetime64: ''' >>> assert(EminiFuture.get_expiry('ESH8') == np.datetime64('2018-03-16T08:30')) ''' month_str = fut_symbol[-2: -1] year_str = fut_symbol[-1:] month = future_code_to_month_number(month_str) assert(isinstance(month, int)) year = 2010 + int(year_str) expiry_date = EminiFuture.calendar.third_friday_of_month(month, year).astype(datetime.date) return np.datetime64(expiry_date) + np.timedelta64(8 * 60 + 30, 'm')
[docs]class EminiOption: calendar = Calendar.get_calendar(Calendar.NYSE)
[docs] @staticmethod def decode_symbol(name: str) -> Tuple[weekday, int, int, int]: ''' >>> EminiOption.decode_symbol('E1AF8') (MO, 2018, 1, 1) ''' if re.match('EW[1-4].[0-9]', name): # Friday year = int('201' + name[-1:]) if year in [2010, 2011]: year += 10 week = int(name[2:3]) month = future_code_to_month_number(name[3:4]) return rd.FR, year, month, week if re.match('E[1-5]A.[0-9]', name): # Monday year = int('201' + name[-1:]) if year in [2010, 2011]: year += 10 week = int(name[1:2]) month = future_code_to_month_number(name[3:4]) return rd.MO, year, month, week if re.match('E[1-5]C.[0-9]', name): # Wednesday year = int('201' + name[-1:]) if year in [2010, 2011]: year += 10 week = int(name[1:2]) month = future_code_to_month_number(name[3:4]) return rd.WE, year, month, week if re.match('EW[A-Z][0-9]', name): # End of month year = int('201' + name[-1:]) if year in [2010, 2011]: year += 10 week = -1 month = future_code_to_month_number(name[2:3]) return rd.WE, year, month, week else: raise Exception(f'could not decode: {name}')
[docs] @staticmethod def get_expiry(symbol: str) -> np.datetime64: ''' >>> EminiOption.get_expiry('EW2Z5') numpy.datetime64('2015-12-11T15:00') >>> EminiOption.get_expiry('E3AF7') numpy.datetime64('2017-01-17T15:00') >>> EminiOption.get_expiry('EWF0') numpy.datetime64('2020-01-31T15:00') ''' assert ':' not in symbol, f'{symbol} contains: pass in option root instead' weekday, year, month, week = EminiOption.decode_symbol(symbol) expiry = get_date_from_weekday(weekday.weekday, year, month, week) if weekday in [rd.WE, rd.FR]: expiry = EminiOption.calendar.add_trading_days(expiry, num_days=0, roll='backward') else: expiry = EminiOption.calendar.add_trading_days(expiry, num_days=0, roll='forward') # Option expirations changed on 9/20/2015 from 3:15 to 3 pm - # See https://www.cmegroup.com/market-regulation/files/15-384.pdf expiry += np.where(expiry < np.datetime64('2015-09-20'), np.timedelta64(15 * 60 + 15, 'm'), np.timedelta64(15, 'h')) return expiry
if __name__ == "__main__": import doctest doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)