Source code for option_util.expirations
import re
from datetime import datetime, timedelta
from typing import Dict, List, Literal, Optional
from dateutil.relativedelta import FR, relativedelta
from pandas.tseries.holiday import USFederalHolidayCalendar
from pandas.tseries.offsets import CustomBusinessDay
ExpirationType = Literal[
'friday', 'third_friday', 'quarter_end', 'next_bus_day', 'wednesday',
'quarter_third_friday',
]
[docs]
def previous_business_day(date: datetime) -> datetime.date:
"""Uses pandas calendars to get the previous business day.
`date` can be any day of the week including weekends.
US Federal holidays are excluded from the results.
:param date: The day to start from.
YYYY-MM-DD format.
:return: datetime
:Example:
>>> # Get the previous business day from date
>>> prev_bus_day = expirations.previous_business_day(
>>> date=datetime(2024, 10, 27)) # Sunday
>>> print(prev_bus_day) # Previous bus day is Friday
Next business day: 2024-10-25
"""
# Custom business day object that excludes weekends and US federal holidays
us_bd = CustomBusinessDay(calendar=USFederalHolidayCalendar())
# Subtract one business day
previous_business_day = date - us_bd
# Convert back to string format
return previous_business_day.date()
[docs]
def next_expiration(
expiration_type: ExpirationType,
from_date: Optional[datetime] = None,
) -> datetime.date:
"""Get the next expiration date based on the expiration type.
:param expiration_type: When the option will expire.
options: 'friday', 'third_friday', 'quarterly', 'next_bus_day',
'wednesday'
:param from_date: The date to make the calculation from.
:return: datetime.date
:Example:
>>> # Return the next expiration by type.
>>> next_exp_by_type = expirations.next_expiration(
... expiration_type='friday', from_date=datetime(2024,10,27)
... )
>>> print(f'Next Expiration by Type: {next_exp_by_type}')
Next Expiration by Type: 2024-11-01
:raises:
NotImplementedError: If the expiration type is not yet implemented.
ValueError: If an invalid expiration type is provided.
"""
"""
Get the next expiration date based on the security type.
Args:
security_type (SecurityType): The type of security (EQUITY, FUTURE,
or VOLATILITY).
from_date (Optional[datetime]): The starting date.
If None, uses the current date.
return_format (None | str): The format string is used to format the
date or return a datetime if None.
Example of inputs, "%Y-%m-%d" or "%Y%m%d"
Returns:
datetime | str: The next expiration date.
Raises:
NotImplementedError: If the security type is not yet implemented.
ValueError: If an invalid security type is provided.
"""
if expiration_type == "friday":
exp = next_friday(from_date)
elif expiration_type == "next_bus_day":
exp = next_business_day(from_date)
elif expiration_type == "third_friday":
exp = next_monthly_expiration(from_date)
elif expiration_type == "quarter_end":
exp = next_quarter_end(from_date)
elif expiration_type == "quarter_third_friday":
exp = next_quarter_third_friday(from_date)
elif expiration_type == "wednesday":
raise NotImplementedError("Wednesday expirations are not implemented "
"yet.")
else:
raise ValueError(f"Invalid security type: {expiration_type}")
return exp
[docs]
def next_business_day(from_date: Optional[datetime] = None) -> datetime.date:
"""Get the next trading day from the given date."""
next_day = from_date + timedelta(days=1)
while next_day.weekday() >= 5: # 5 = Saturday, 6 = Sunday
next_day += timedelta(days=1)
return next_day.date()
[docs]
def next_friday(from_date: Optional[datetime] = None) -> datetime.date:
"""Get the date of the next Friday from the given date."""
days_ahead = 4 - from_date.weekday()
if days_ahead <= 0: # Target day already happened this week
days_ahead += 7
return (from_date + timedelta(days=days_ahead)).date()
[docs]
def next_monthly_expiration(
from_date: Optional[datetime] = None) -> datetime.date:
"""
Get the date of the next monthly options expiration
(third Friday of the month).
"""
third_friday = from_date.replace(day=1) + relativedelta(weekday=FR(3))
if from_date >= third_friday:
third_friday = from_date.replace(day=1) + relativedelta(months=1,
weekday=FR(3))
return third_friday.date()
[docs]
def next_quarter_end(from_date: Optional[datetime] = None) -> datetime.date:
"""
Get the last Friday of the last month in the current quarter.
"""
quarter_month = ((from_date.month - 1) // 3) * 3 + 3
quarter_end = from_date.replace(month=quarter_month,
day=1) + relativedelta(months=1, days=-1)
last_friday = quarter_end + relativedelta(weekday=FR(-1))
if from_date > last_friday or (
from_date.date() == last_friday.date() and
from_date.time() >= last_friday.time()):
next_quarter_end = quarter_end + relativedelta(months=3)
last_friday = next_quarter_end + relativedelta(weekday=FR(-1))
return last_friday.date()
[docs]
def next_quarter_third_friday(
from_date: Optional[datetime] = None) -> datetime.date:
"""Get the third Friday of the last month of the current quarter."""
quarter_month = ((from_date.month - 1) // 3) * 3 + 3
third_friday = from_date.replace(month=quarter_month,
day=1) + relativedelta(weekday=FR(3))
if from_date >= third_friday:
next_quarter_month = (quarter_month % 12) + 3
next_quarter_year = from_date.year + (
1 if next_quarter_month < quarter_month else 0)
third_friday = datetime(next_quarter_year, next_quarter_month,
1) + relativedelta(weekday=FR(3))
return third_friday.date()
[docs]
def future_expirations(from_date: Optional[datetime] = None
) -> Dict[str, datetime]:
"""Get a list of future options expiration dates."""
return {
'from_date': from_date.date(),
'previous_business_day': previous_business_day(from_date),
'next_business_day': next_business_day(from_date),
'next_friday': next_friday(from_date),
'next_quarter_end': next_quarter_end(from_date),
'next_monthly_expiration': next_monthly_expiration(from_date),
'next_quarter_third_friday': next_quarter_third_friday(from_date)
}
[docs]
def parse_contract_symbol(symbol: str) -> float:
"""
Parse the contract symbol to extract the strike price.
Args:
symbol (str): The contract symbol string.
Returns:
float: The strike price extracted from the symbol.
"""
pattern = r'([\w ]{6})((\d{2})(\d{2})(\d{2}))([PC])(\d{8})'
match = re.match(pattern, symbol)
if match:
return float(match.group(
7)) / 1000 # Divide by 1000 to get the correct strike price
return float('nan')