Skip to main content

Extensions of the exchange-calendars package

Project description

exchange-calendars-extensions

PyPI Python Support PyPI Downloads

A Python package that transparently adds some features to the exchange-calendars package.

For all exchanges:

  • Calendars that aggregate existing regular and ad-hoc holidays/special open days/special close days, respectively, into a single calendar.
  • Calendars for the last trading session of each month, and the last regular trading session of each month.
  • Calendars for weekend and week days.
  • The ability to modify the properties of days at runtime.
  • The ability to attach and query arbitrary tags to days.

For select exchanges:

  • Calendars for additional special trading sessions, such as monthly and quarterly expiry days (aka quadruple witching).

Aggregate calendars

These additional calendars aggregate regular and ad-hoc holidays/special open/special close days, respectively, into a single calendar. This can be convenient to avoid having to querying multiply calendars on the original exchange calendar.

Note: Aggregate calendars for special open/close days may combine days with different session times. From the aggregate calendar alone, the open/close time cannot be recovered.

Additional calendars

In addition to information that is already available in exchange-calendars, this package also adds calendars for

  • the last trading session of each month,
  • the last regular trading session of each month, and
  • weekend and week days, as per the underlying weekmask.

For select exchanges (see below), this package also adds calendars for:

  • quarterly expiry days (aka quadruple witching), and
  • monthly expiry days (in all remaining months that don't have a quarterly expiry day).

Calendar modifications and tags

Ideally, exchange calendars from exchange-calendars always provide a correct view. In reality though, exchanges sometimes adjust their trading schedule, in some instances even on short notice. This poses a challenge as the typical release cycles implies that necessary adjustments to adapt the calendars to reality may not be possible in a timely manner.

To address this issue, this package adds the ability to modify exchange calendars at runtime. This can generally convert any day into a business or non-business day with the desired properties. In addition, arbitrary tags can be attached to days, e.g. to group days into custom categories.

Installation

The package is available on PyPI and can be installed via pip, Poetry or any other dependency management tool.

pip install exchange-calendars-extensions

General usage

Note: In general, any code snippet in this documentation is self-contained and should execute successfully in a fresh Python interpreter instance.

Import exchange_calendars_extensions and register extended exchange calendar classes with the exchange_calendars module.

import exchange_calendars_extensions as ecx

ecx.apply_extensions()

This replaces the default exchange calendar classes with their extended versions.

Get an exchange calendar instance and verify that extended exchange calendars are subclasses of the abstract base class ecx.ExtendedExchangeCalendar. This class inherits both from ec.ExchangeCalendar and the new protocol class ecx.ExchangeCalendarExtensions which defines the extended properties.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx

ecx.apply_extensions()

calendar = ec.get_calendar("XLON")

# It's still a regular exchange calendar.
assert isinstance(calendar, ec.ExchangeCalendar)

# But it's also an extended exchange calendar...
assert isinstance(calendar, ecx.ExtendedExchangeCalendar)
# ...and implements the extended protocol.
assert isinstance(calendar, ecx.ExchangeCalendarExtensions)

The original classes can be re-instated by calling ecx.remove_extensions().

import exchange_calendars as ec
import exchange_calendars_extensions as ecx

ecx.apply_extensions()

...

ecx.remove_extensions()

calendar = ec.get_calendar("XLON")

# It's a regular exchange calendar.
assert isinstance(calendar, ec.ExchangeCalendar)

# But it's not an extended exchange calendar anymore.
assert not isinstance(calendar, ecx.ExtendedExchangeCalendar)
assert not isinstance(calendar, ecx.ExchangeCalendarExtensions)

Additional properties

Extended exchange calendars have additional properties:

Property Description
holidays_all Regular and ad-hoc holidays in single calendar.
special_opens_all Regular and ad-hoc special open days in a single calendar.
special_closes_all Regular and ad-hoc special close days in a single calendar.
weekend_days Weekend days, as defined by the underlying weekmask, in a single calendar.
week_days Week days (the complement of weekend days) in a single calendar.
quarterly_expiries Quarterly expiry days, also known as quadruple witching.

Many exchanges observe special business days on which market index futures, options futures, stock options and stock futures expire, typically resulting in increased volatility and traded volume. Quadruple witching is typically observed on the third Friday of March, June, September and December, although some exchanges observe it on Thursday instead. Note that in the case of collisions with holidays or special open/close days, a quarterly expiry day is usually rolled backward to the previous regular business day.
monthly_expiries Monthly expiry days. Similar to quarterly expiry days, but for all remaining months of the year. Provided in a separate calendar as they typically result in less extreme trading patterns.
last_session_of_months Calendar with the last trading session for each month of the year.
last_regular_session_of_months Calendar with the last regular trading session of each month of the year, i.e. not a special open/close or otherwise irregular day.
last_regular_session_of_months Calendar with the last regular trading session of each month of the year, i.e. not a special open/close or otherwise irregular day.
tags Method to query day ranges for given tags.

For example,

import exchange_calendars as ec
import exchange_calendars_extensions as ecx

ecx.apply_extensions()

calendar = ec.get_calendar("XLON")
print(
    calendar.holidays_all.holidays(
        start="2020-01-01", end="2020-12-31", return_name=True
    )
)

will output

2020-01-01         New Year's Day
2020-04-10            Good Friday
2020-04-13          Easter Monday
2020-05-08                    NaN
2020-05-25    Spring Bank Holiday
2020-08-31    Summer Bank Holiday
2020-12-25              Christmas
2020-12-26             Boxing Day
2020-12-28     Weekend Boxing Day
dtype: object

Note: NumPy's NaN indicates holidays without a specific name in exchange-calendars's model, e.g. ad-hoc holidays such as 2020-05-08 (May Day bank holiday was moved in honor of the 75th VE Day anniversary).

Quarterly and monthly expiry days

import exchange_calendars as ec
import exchange_calendars_extensions as ecx

ecx.apply_extensions()

calendar = ec.get_calendar("XLON")
print(calendar.quarterly_expiries.holidays(start="2023-01-01", end="2023-12-31"))
print(calendar.monthly_expiries.holidays(start="2023-01-01", end="2023-12-31"))

will output

DatetimeIndex(['2023-03-17', '2023-06-16', '2023-09-15', '2023-12-15'], dtype='datetime64[us]', freq=None)
DatetimeIndex(['2023-01-20', '2023-02-17', '2023-04-21', '2023-05-19',
               '2023-07-21', '2023-08-18', '2023-10-20', '2023-11-17'],
              dtype='datetime64[us]', freq=None)

Note: Expiry day calendars do not provide names so using return_name=True only makes sens if you definitely need a Pandas Series and not a DatetimeIndex.

Last trading days of months

Similar to the expiry day calendars, the last (regular) trading day of a month calendars do not provide names.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx

ecx.apply_extensions()

calendar = ec.get_calendar("XLON")
print(
    calendar.last_trading_days_of_months.holidays(start="2023-01-01", end="2023-12-31")
)
print(
    calendar.last_regular_trading_days_of_months.holidays(
        start="2023-01-01", end="2023-12-31"
    )
)

will output

DatetimeIndex(['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-28',
               '2023-05-31', '2023-06-30', '2023-07-31', '2023-08-31',
               '2023-09-29', '2023-10-31', '2023-11-30', '2023-12-29'],
              dtype='datetime64[us]', freq=None)
DatetimeIndex(['2023-01-31', '2023-02-28', '2023-03-31', '2023-04-28',
               '2023-05-31', '2023-06-30', '2023-07-31', '2023-08-31',
               '2023-09-29', '2023-10-31', '2023-11-30', '2023-12-28'],
              dtype='datetime64[us]', freq=None)

Notice the difference in December where 2023-12-29 is a special close day, so 2023-12-28 is the last regular trading day in that month for XLON.

Calendar changes

Extended exchange calendars not only provide additional properties, they can also be adjusted at runtime. This allows you, for example, to convert a regular business day into a special open/close day or a holiday, or to convert a weekend day into a regular trading day.

The model

Before looking at how changes are effected, it is important to consider the model used to represent changes. To that end, let's consider a single day d in an exchange calendar. Of course, d has certain properties in the yet unchanged exchange calendar. It may be a regular business day, a holiday, or something else. This is the pristine state of d.

A change to d describes a layer that can be put on top of d's current state to change some or all of its properties. It is described by the DayChange Pydantic model and sub-models:

from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING
from exchange_calendars_extensions import DaySpec


class DayChange(BaseModel):
    spec: DaySpec | None | MISSING = MISSING
    name: str | None | MISSING = MISSING
    tags: set[str] | MISSING = MISSING

Notice that this uses the experimental MISSING sentinel introduced in Pydantic 2.12.0 which indicates undefined fields. This is useful instead of using None as a sentinel since Nonemay have a well-defined meaning other than an undefined value for certain fields. For a DayChange, a field with a value of MISSING means that the field is unspecified in the change and so the corresponding underlying properties of the day are left unchanged. This way, a DayChange can represent a delta or a partial layer to apply on top of the current state of a day.

The fields have the following meaning:

Property Description
spec Core properties of the business or non-business day.
name The name of the day.
tags A set of tags.

The name property can be used to assign a name to a day, but note that this will only be visible if the day is a regular holiday or regular special open/close day.

DaySpec

The spec property describes the core properties of the day, depending on whether it is a business day or a non-business day. The corresponding DaySpec type is a discriminated union of BusinessDaySpec and NonBusinessDaySpec.

from typing import Annotated
from pydantic import Field
from exchange_calendars_extensions import NonBusinessDaySpec, BusinessDaySpec

DaySpec = Annotated[
    NonBusinessDaySpec | BusinessDaySpec, Field(discriminator="business_day")
]

A non-business day, is described by NonBusinessDaySpec.

from typing import Literal
from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING


class NonBusinessDaySpec(BaseModel):
    business_day: Literal[False] = False
    weekend_day: bool | MISSING = MISSING
    holiday: bool | MISSING = MISSING

It can be either a weekend day or a holiday, or both. Note that validation ensures that at least one of weekend_day or holiday is True.

A business day is described by BusinessDaySpec.

from typing import Literal
from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING
from exchange_calendars_extensions.datetime import TimeLike


class BusinessDaySpec(BaseModel):
    business_day: Literal[True] = True
    open: TimeLike | Literal["regular"] | MISSING = MISSING
    close: TimeLike | Literal["regular"] | MISSING = MISSING

A business day must have a trading session with a defined open and close time. The times can be specified explicitly or implicitly as a reference to the regular open/close time (regular). A MISSING value uses the open/close time of the underlying unmodified day, if it is a business day, or the regular open/close time, otherwise.

Here, TimeLike is a datetime.time subtype that can be used with Pydantic and supports initialization from strings in HH:MM or HH:MM:SS format for convenience.

Tags

Tags are sets of typically short strings that can be used to group related days.

Examples

The DayChange model can express a wide range of complete or partial changes to a day. For example, the following change only sets the name of a day, but leaves everything else unchanged

from exchange_calendars_extensions import DayChange

DayChange(name="Holiday")

Such change applied to a day that is either a regular holiday or regular special open/close day will only alter its name. But note that application to a regular business day will not have any effect as only calendars for regular special days support names on days.

The following change describes a special open day:

from exchange_calendars_extensions import DayChange, BusinessDaySpec
import datetime as dt

DayChange(
    spec=BusinessDaySpec(open=dt.time(11, 0), close="regular"), name="Special Open"
)

It can be applied to any business or non-business day to convert it into a special open day. The optional name can be left undefined here to retain the existing name (or None) of the day.

Likewise, the following change describes a weekend day that is also a holiday, and assigns some tags as well

from exchange_calendars_extensions import DayChange, NonBusinessDaySpec

DayChange(
    spec=NonBusinessDaySpec(weekend_day=True, holiday=True),
    name="Weekend Holiday",
    tags={"foo", "bar"},
)

Applying calendar changes

To apply a change to a single day, use the change_day(...) method.

For example, to add a new holiday:

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import DayChange, NonBusinessDaySpec
import pandas as pd

ecx.apply_extensions()

d = pd.Timestamp("2022-12-28")

calendar = ec.get_calendar("XLON")

assert d not in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d not in calendar.holidays_all.holidays()
assert d not in calendar.weekend_days.holidays()
assert calendar.day.rollforward(d) == d

ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

calendar = ec.get_calendar("XLON")

assert d in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d in calendar.holidays_all.holidays()
assert d not in calendar.weekend_days.holidays()  # It's not a weekend day.
assert (
        calendar.regular_holidays.holidays(start=d, end=d, return_name=True)[d] == "Holiday"
)
assert calendar.day.rollforward(d) == pd.Timestamp("2022-12-29")

Stacked changes to a single day

A change to a day becomes part of the calendar's state. You can apply a second change to the same day to adjust the properties again. By default, the second change will be applied on top of the first change.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import DayChange, NonBusinessDaySpec
import pandas as pd

ecx.apply_extensions()

d = pd.Timestamp("2022-12-28")

ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

calendar = ec.get_calendar("XLON")

assert d in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d in calendar.holidays_all.holidays()
assert d not in calendar.weekend_days.holidays()
assert (
        calendar.regular_holidays.holidays(start=d, end=d, return_name=True)[d] == "Holiday"
)
assert calendar.day.rollforward(d) == pd.Timestamp("2022-12-29")

ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(weekend_day=True), name="Changed again"),
)

calendar = ec.get_calendar("XLON")

assert d in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d in calendar.holidays_all.holidays()
assert d in calendar.weekend_days.holidays()  # It's now a weekend day, too.
assert (
        calendar.regular_holidays.holidays(start=d, end=d, return_name=True)[d]
        == "Changed again"
)
assert calendar.day.rollforward(d) == pd.Timestamp("2022-12-29")

When a second change incoming gets applied on top of an existing change current, both changes are merged. Generally, if a property is specified only in actual or in incoming, but not the other, it is retained in the result, and if both changes do not specify it, then so does the result. However, if the property is specified in both changes, they are merged as follows:

Property Merging Behavior
spec (same type) Merge the specs on their properties, where conflicting, use the value from incoming.
spec (different types) Use the spec from incoming.
name Use the name from incoming
tags Use the set union of tags from actualand incoming.

See the following example:

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
)
import pandas as pd

ecx.apply_extensions()

d = pd.Timestamp("2022-12-28")

ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(weekend_day=True), name="Changed again"),
)

calendar = ec.get_calendar("XLON")

assert d in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d in calendar.holidays_all.holidays()
assert d in calendar.weekend_days.holidays()  # It's now a weekend day, too.
assert d not in calendar.week_days.holidays()
assert (
        calendar.regular_holidays.holidays(start=d, end=d, return_name=True)[d]
        == "Changed again"
)
assert calendar.day.rollforward(d) == pd.Timestamp("2022-12-29")

ecx.change_day("XLON", date="2022-12-28", action=DayChange(spec=BusinessDaySpec()))

calendar = ec.get_calendar("XLON")

assert d not in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d not in calendar.holidays_all.holidays()
assert d not in calendar.weekend_days.holidays()
assert d in calendar.week_days.holidays()  # It's a regular business day, again.
assert calendar.day.rollforward(d) == d

Reverting changes

To remove any changes to a day and recover the original state, use change_day(...) with the CLEAR sentinel:

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
    NonBusinessDaySpec,
    CLEAR,
)
import pandas as pd

ecx.apply_extensions()

d = pd.Timestamp("2022-12-28")

ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(
        spec=NonBusinessDaySpec(holiday=True, weekend_day=True), name="Holiday"
    ),
)

calendar = ec.get_calendar("XLON")

assert d in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d in calendar.holidays_all.holidays()
assert d in calendar.weekend_days.holidays()
assert d not in calendar.week_days.holidays()
assert (
        calendar.regular_holidays.holidays(start=d, end=d, return_name=True)[d] == "Holiday"
)
assert calendar.day.rollforward(d) == pd.Timestamp("2022-12-29")

ecx.change_day("XLON", date="2022-12-28", action=CLEAR)

calendar = ec.get_calendar("XLON")

assert d not in calendar.regular_holidays.holidays()
assert d not in calendar.adhoc_holidays
assert d not in calendar.holidays_all.holidays()
assert d not in calendar.weekend_days.holidays()
assert d in calendar.week_days.holidays()
assert calendar.day.rollforward(d) == d

It is not possible to revert select fields of an already applied change back to an undefined state through another change. For example, you cannot first change the name of a day with one change, and then clear the name field through a second change applied on top. The only way is to reset the day and then apply the desired change.

Specifying dates, times, and day types

Thanks to Pydantic's runtime validation, most functions accept dates, times, day changes, et cetera in different formats and parse them automatically into the correct type.

For dates, you may use pandas.Timestamp, datetime.date, or simply a strings in ISO format YYYY-MM-DD.

For times, you can use datetime.time, strings in the format HH:MM:SS or HH:MM. To specify a session open or close time for a day modification, you can also use the sentinel 'regular' to refer to the prevailing standard time.

Visibility of Changes

Whenever a calendar is modified, the changes are only reflected after obtaining a new instance.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
    NonBusinessDaySpec,
    BusinessDaySpec,
)

ecx.apply_extensions()

calendar = ec.get_calendar("XLON")

# Unchanged calendar.
assert "2022-12-27" in calendar.holidays_all.holidays()
assert "2022-12-28" not in calendar.holidays_all.holidays()

# Modify calendar. This clears the cache, so ec.get_calendar('XLON') will return a new instance next time.
ecx.change_day(
    "XLON",
    date="2022-12-27",
    action=DayChange(spec=BusinessDaySpec(open="regular", close="regular")),
)
ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

# Changes not reflected in existing instance.
assert "2022-12-27" in calendar.holidays_all.holidays()
assert "2022-12-28" not in calendar.holidays_all.holidays()

# Get new instance.
calendar = ec.get_calendar("XLON")

# Changes reflected in new instance.
assert "2022-12-27" not in calendar.holidays_all.holidays()
assert "2022-12-28" in calendar.holidays_all.holidays()

# Revert the changes.
ecx.remove_changes("XLON")

# Get new instance.
calendar = ec.get_calendar("XLON")

# Changes reverted in new instance.
assert "2022-12-27" in calendar.holidays_all.holidays()
assert "2022-12-28" not in calendar.holidays_all.holidays()

Changesets

So far, we have considered only changes to a single day which become part of the affected calendar's state. This state is captured in a ChangeSet which is just a dictionary mapping dates to changes.

To make working with multiple changes easier, it is possible to create and apply entire changesets at once.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
)

ecx.apply_extensions()

calendar = ec.get_calendar("XLON")

assert "2022-12-27" in calendar.holidays_all.holidays()
assert "2022-12-28" not in calendar.holidays_all.holidays()

changeset = {
    "2022-12-27": DayChange(spec=BusinessDaySpec()),
    "2022-12-28": DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
}

ecx.change_calendar("XLON", changeset)

calendar = ec.get_calendar("XLON")

assert "2022-12-27" not in calendar.holidays_all.holidays()
assert "2022-12-28" in calendar.holidays_all.holidays()

The function change_calendar() supports an optional argument mode which can be set to "replace", "update", or "merge" to control how the changes are applied.

  • The default is "merge", which merges existing and new changes on a per-day basis, just how change_day() works.
  • When set to "update", the existing changeset is just updated with the new changes, so an incoming change always
  • overwrites an existing change.
  • When set to "replace", the existing changeset is replaced entirely with the new changes.

The change_calendar function can also be used to clear all changes to a calendar:

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
)

ecx.apply_extensions()

changeset = {
    "2022-12-27": DayChange(spec=BusinessDaySpec()),
    "2022-12-28": DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
}

ecx.change_calendar("XLON", changeset)

calendar = ec.get_calendar("XLON")

assert "2022-12-27" not in calendar.holidays_all.holidays()
assert "2022-12-28" in calendar.holidays_all.holidays()

ecx.change_calendar("XLON", {}, mode="replace")

calendar = ec.get_calendar("XLON")

assert "2022-12-27" in calendar.holidays_all.holidays()
assert "2022-12-28" not in calendar.holidays_all.holidays()

For convenience, the function remove_changes(exchange: str | None) also clears all changes for a given exchange, or for all exchanges if exchange is None.

An incoming changeset may contain CLEAR entries to remove changes for specific dates.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
    CLEAR,
)

ecx.apply_extensions()

changeset = {
    "2022-12-27": DayChange(spec=BusinessDaySpec()),
    "2022-12-28": DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
}

ecx.change_calendar("XLON", changeset)

calendar = ec.get_calendar("XLON")

assert "2022-12-27" not in calendar.holidays_all.holidays()
assert "2022-12-28" in calendar.holidays_all.holidays()

changeset_2 = {
    "2022-12-27": CLEAR,
}
ecx.change_calendar("XLON", changeset_2)

calendar = ec.get_calendar("XLON")

assert "2022-12-27" in calendar.holidays_all.holidays()
assert "2022-12-28" in calendar.holidays_all.holidays()

Retrieving the Changeset for a Calendar

You can inspect the current state of a calendar directly to verify what changes have been applied using get_changes(exchange: str | None).

from pprint import pprint
from pydantic import TypeAdapter
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    ChangeSet,
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
)

ecx.apply_extensions()

ecx.change_day("XLON", date="2022-12-27", action=DayChange(spec=BusinessDaySpec()))
ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

changes: ChangeSet = ecx.get_changes("XLON")

pprint(changes)

print("\n")

ta = TypeAdapter(ChangeSet)
print(ta.dump_json(changes, indent=2).decode())

This will output

{Timestamp('2022-12-27 00:00:00'): DayChange(type='change', spec=BusinessDaySpec(business_day=True, open=<MISSING>, close=<MISSING>), name=<MISSING>, tags=<MISSING>),
 Timestamp('2022-12-28 00:00:00'): DayChange(type='change', spec=NonBusinessDaySpec(business_day=False, weekend_day=<MISSING>, holiday=True), name='Holiday', tags=<MISSING>)}
{
  "2022-12-27 00:00:00": {
    "type": "change",
    "spec": {
      "business_day": true
    }
  },
  "2022-12-28 00:00:00": {
    "type": "change",
    "spec": {
      "business_day": false,
      "holiday": true
    },
    "name": "Holiday"
  }
}

Serializing to JSON makes changes easy to store and pass around.

from pydantic import TypeAdapter
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    ChangeSet,
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
)

ecx.apply_extensions()

ecx.change_day("XLON", date="2022-12-27", action=DayChange(spec=BusinessDaySpec()))
ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

changes: ChangeSet = ecx.get_changes("XLON")

ta = TypeAdapter(ChangeSet)

serialized = ta.dump_json(changes)

changes_2 = ta.validate_json(serialized)

ecx.change_calendar("XETR", changes_2, mode="replace")

print(ta.dump_json(ecx.get_changes("XETR"), indent=2).decode())

Set exchange to None to retrieve a dictionary of all changesets for all exchanges.

from pydantic import TypeAdapter
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    ChangeSet,
    DayChange,
    BusinessDaySpec,
    NonBusinessDaySpec,
)

ecx.apply_extensions()

ecx.change_day("XLON", date="2022-12-27", action=DayChange(spec=BusinessDaySpec()))
ecx.change_day(
    "XLON",
    date="2022-12-28",
    action=DayChange(spec=NonBusinessDaySpec(holiday=True), name="Holiday"),
)

ecx.change_day("XETR", date="2022-12-30", action=DayChange(spec=BusinessDaySpec()))

changes: dict[str, ChangeSet] = ecx.get_changes()

ta = TypeAdapter(dict[str, ChangeSet])

print(ta.dump_json(changes, indent=2).decode())

This prints

{
  "XLON": {
    "2022-12-27 00:00:00": {
      "type": "change",
      "spec": {
        "business_day": true
      }
    },
    "2022-12-28 00:00:00": {
      "type": "change",
      "spec": {
        "business_day": false,
        "holiday": true
      },
      "name": "Holiday"
    }
  },
  "XETR": {
    "2022-12-30 00:00:00": {
      "type": "change",
      "spec": {
        "business_day": true
      }
    }
  }
}

Tags

Custom tags can be added through calendar changes as well.

import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
)

ecx.apply_extensions()

changeset = {
    "2022-12-27": DayChange(tags={"foo", "bar"}),
    "2022-12-28": DayChange(tags={"foo", "foobar"}),
}

ecx.change_calendar("XLON", changeset)

calendar = ec.get_calendar("XLON")

print(calendar.tags(return_tags=True))

This will output

2022-12-27       {foo, bar}
2022-12-28    {foo, foobar}
dtype: object

Like other holiday calendars, this function either returns a pandas.DatetimeIndex when return_tags=False (the default) or a pandas.Series when return_tags=True.

Likewise, dates can be filtered via start and end arguments, as well as a set of required tags.

import pandas as pd
import exchange_calendars as ec
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    DayChange,
)

ecx.apply_extensions()

changeset = {
    "2022-12-27": DayChange(tags={"foo", "bar"}),
    "2022-12-28": DayChange(tags={"foo", "foobar"}),
}

ecx.change_calendar("XLON", changeset)

calendar = ec.get_calendar("XLON")

print(calendar.tags(start=pd.Timestamp("2022-12-28"), return_tags=True))
print("\n")
print(calendar.tags(end=pd.Timestamp("2022-12-27"), return_tags=True))
print("\n")
print(calendar.tags(tags={"foo"}, return_tags=True))

The result:

2022-12-28    {foo, foobar}
dtype: object

2022-12-27    {bar, foo}
dtype: object

2022-12-27       {bar, foo}
2022-12-28    {foo, foobar}
dtype: object

When the tags argument is given, all provided tags must be present on a day to match the filter.

Supported exchanges for monthly/quarterly expiry

This package currently provides support for monthly/quarterly expiry calendars for the following subset of exchanges from exchange_calendars:

  • ASEX
  • BMEX
  • XAMS
  • XBRU
  • XBUD
  • XCSE
  • XDUB
  • XETR
  • XHEL
  • XIST
  • XJSE
  • XLIS
  • XLON
  • XMAD
  • XMIL
  • XNAS
  • XNYS
  • XOSL
  • XPAR
  • XPRA
  • XSTO
  • XSWX
  • XTAE
  • XTSE
  • XWAR
  • XWBO

Advanced usage

Adding an extended calendar for a new exchange

To facilitate the creation of extended exchange calendar classes, the function extend_class is provided in the submodule exchange_calendars_extensions.holiday_calendar.

from exchange_calendars.exchange_calendar_xlon import XLONExchangeCalendar
from exchange_calendars_extensions import extend_class

xlon_extended_cls = extend_class(XLONExchangeCalendar, day_of_week_expiry=4)

The first argument to extend_class should be the class of the exchange calendar to extend. The second and optional parameter, which defaults to None, is the day of the week on which expiry days are normally observed. If this parameter is None, this assumes that the underlying exchange does not support monthly or quarterly expiry days and the respective calendars will not be added.

The returned extended class directly inherits from the passed base class and adds the additional attributes like holidays_all et cetera. The returned class also supports programmatic modifications using the corresponding exchange key of the parent class.

To register a new extended class for an exchange, use the register_extension() function before calling apply_extensions().

from exchange_calendars_extensions import register_extension, apply_extensions

register_extension("XLON", day_of_week_expiry=4)
apply_extensions()
...

Here, key should be the name, i.e. not an alias, under which the extended class is registered with the exchange_calendars package, and cls should be the extended class.

Caveat: Merging holiday calendars

For the various calendars, exchange-calendars defines and uses the class exchange_calendars.exchange_calendar.HolidayCalendar which is a direct subclass of the abstract base class pandas.tseries.holiday.AbstractHolidayCalendar.

One of the assumptions of AbstractHolidayCalendar is that each contained rule that defines a holiday has a unique name. Thus, when merging two calendars via the .merge() method, the resulting calendar will only retain a single rule for each name, eliminating any duplicates.

This creates a problem with the calendars provided by this package. For example, constructing the holiday calendar backing holidays_all requires to add a rule for each ad-hoc holiday. However, since ad-hoc holidays don't define a unique name, each rule would either have to generate a unique name for itself, or use the same name as the other rules. This package uses the latter approach, i.e. all ad-hoc holidays are assigned the same name ad-hoc holiday.

As a result, the built-in merge functionality of AbstractHolidayCalendar would eliminate all but one of the ad-hoc holidays when merging with another calendar. This is not the desired behavior.

To avoid this problem, this package defines the function merge_calendars(calendars: Iterable[AbstractHolidayCalendar]) which returns a calendar that simply concatenates, in order, all rules from the passed-in calendars. The returned calendar is a subclass of HolidayCalendar that handles possible duplicates by filtering them out before returning from a call to holidays().

In essence: Always use merge_calendars(...) instead of AbstractHolidayCalendar.merge(...) when merging involves any of the calendars added by this package. Keep in mind that for duplicate elimination, rules more to the front of the list have higher priority.

Contributing

Contributions are welcome. Please open an issue or submit a pull request on GitHub.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

exchange_calendars_extensions-0.10.2.tar.gz (69.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

exchange_calendars_extensions-0.10.2-py3-none-any.whl (43.1 kB view details)

Uploaded Python 3

File details

Details for the file exchange_calendars_extensions-0.10.2.tar.gz.

File metadata

File hashes

Hashes for exchange_calendars_extensions-0.10.2.tar.gz
Algorithm Hash digest
SHA256 e47a2b0407e371766c6c8a3434e34ac035a0f344fa05bc3af8e53289a2598a1a
MD5 2c20c4447a390d1503a801361b50e6b6
BLAKE2b-256 fd6d1ac65a7c844c9bddca4136d01e1df26335f6c7b4084a492176c2418a6620

See more details on using hashes here.

Provenance

The following attestation bundles were made for exchange_calendars_extensions-0.10.2.tar.gz:

Publisher: publish.yml on jenskeiner/exchange_calendars_extensions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exchange_calendars_extensions-0.10.2-py3-none-any.whl.

File metadata

File hashes

Hashes for exchange_calendars_extensions-0.10.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5b14dd9438bf4769bf032973cb459fa4223a5fc745fda64157597580957527a5
MD5 7f6544daaabe1617f923e2df7ae2e39e
BLAKE2b-256 5e45ab0d600f60f334bd69b7d73a2d1307ecce0ac6a8bb00849c8b782691357a

See more details on using hashes here.

Provenance

The following attestation bundles were made for exchange_calendars_extensions-0.10.2-py3-none-any.whl:

Publisher: publish.yml on jenskeiner/exchange_calendars_extensions

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page