Skip to content

Changesets

Individual day changes can be accumulated in a ChangeSet — a dictionary mapping dates to DayChange instances. Rather than modifying days one at a time, you can create and apply an entire changeset to an exchange calendar at once.

Applying a changeset

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()

Modes

change_calendar() accepts an optional mode argument that controls how the incoming changeset interacts with any existing changes:

"merge" (default)

Merges incoming and existing changes per day, following the same merge rules as change_day().

"update"

Updates the existing changeset: an incoming change always overwrites the existing change for the same date.

"replace"

Replaces the entire existing changeset with the incoming one.

Clearing changes

Pass an empty changeset with mode="replace" to remove every change for 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, remove_changes(exchange) also clears all changes for a given exchange.

Clearing individual days

An incoming changeset can contain CLEAR entries to remove the change for specific dates while leaving other dates untouched:

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 changes

Use get_changes(exchange) to inspect the current changeset for a calendar:

from pprint import pprint
from pydantic import TypeAdapter
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    ChangeSetDelta,
    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: ChangeSetDelta = ecx.get_changes("XLON")

pprint(changes)

print("\n")

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

{
    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"
  }
}

Note

As far as typing goes, there is a small but important distinction between a changeset as accepted by change_calendar() and the internal changeset where changes accumulate as part of a calendar's state.

The former may also contain CLEAR entries to remove specific days from the internal state when applied on top of it. The end result, that is, the new state, always only contains DayChange entries to describe the actual deviations from the vanilla calendar.

Therefore, change_calendar() accepts ChangeSetDelta instances while get_changes() always returns ChangeSet instances with the only difference being that the former may contain CLEAR entries and the latter not.

Serialization & Deserialization

Serializing to JSON makes changesets easy to store and transfer.

from pydantic import TypeAdapter
import exchange_calendars_extensions as ecx
from exchange_calendars_extensions.changes import (
    ChangeSetDelta,
    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: ChangeSetDelta = ecx.get_changes("XLON")

ta = TypeAdapter(ChangeSetDelta)

# Serialize changeset.
serialized = ta.dump_json(changes)

# Deserialize again.
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())

{
  "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"
  }
}