Skip to content

jenskeiner/exchange_calendar_service

Repository files navigation

Exchange Calendar Service

PyPI Python Support PyPI Downloads

An simple HTTP service for querying exchange calendars for stock exchanges. Built on top of exchange_calendars and exchange_calendars_extensions.

Features

  • RESTful API for exchange calendar queries.
  • Support for 60+ global exchanges.
  • Query holidays, special open/close days, monthly and quarterly expiry days, and more.
  • Support for customization hooks.
  • Docker image available for easy deployment.

Installation

The package requires Python 3.11 or later.

As a tool

If you are primarily interested in running the service as a tool and without any customizations, you can use uv's tool support:

uvx exchange-calendar-service

This will start the service via Uvicorn on http://localhost:8080 by default. See http://localhost:8080/docs for auto-generated API docs.

Alternatively, install and run via pipx:

pipx install exchange-calendar-service
exchange-calendar-service

As a dependency

Add the PyPI package as a dependency to your Python project via uv:

uv add exchange-calendar-service

Or edit pyproject.toml directly:

[project]
dependencies = [
    "exchange-calendar-service=^0.1.0",
]

In a Python virtual environment, you can start the service via a script:

exchange-calendar-service

or by running the Python module directly:

python -m exchange_calendar_service

Container image

For easy deployment, the service is available as a ready-to-use container image on GitHub Container Registry.

docker run -it --rm -p 8080:8080 ghcr.io/jenskeiner/exchange_calendar_service:latest

Examples

Assuming the service is running on http://localhost:8080, here are some examples using curl. Note that you can also conveniently use the auto-generated API docs at http://localhost:8080/docs to try out the endpoints.

Supported exchanges

curl "http://localhost:8080/v1/exchanges"

returns a list of supported exchange MIC codes.

[
  "XAMS",
  "XBRU",
  "XBUD",
  "XCSE",
  "XDUB",
  "XETR",
  "XHEL",
  "XIST",
  "XLIS",
  "XLON",
  "XMAD",
  "XOSL",
  "XPAR"
]

Information about a specific exchange

curl "http://localhost:8080/v1/exchanges/XLON"

returns Information about the London Stock Exchange.

{
  "mic": "XLON",
  "tz": "Europe/London"
}

Describe a day on an exchange

curl "http://localhost:8080/v1/exchanges/XLON/days/2024-03-12"

Result (business day):

{
  "date": "2024-03-12",
  "business_day": true,
  "session": {
    "open": "08:00:00",
    "close": "16:30:00"
  },
  "tags": [
    "regular"
  ]
}
curl "http://localhost:8080/v1/exchanges/XLON/days/2024-12-15"

Result (non-business day):

{
  "date": "2024-12-15",
  "business_day": false,
  "tags": [
    "weekend"
  ]
}

Query days in a date range:

curl "http://localhost:8080/v1/exchanges/XLON/days?start=2024-12-23&end=2024-12-27"

Returns a list of descriptions of the days in range.

[
  {
    "date": "2024-12-23",
    "business_day": true,
    "session": {
      "open": "08:00:00",
      "close": "16:30:00"
    },
    "tags": [
      "regular"
    ]
  },
  {
    "date": "2024-12-24",
    "name": "Christmas Eve",
    "business_day": true,
    "session": {
      "open": "08:00:00",
      "close": "12:30:00"
    },
    "tags": [
      "special close"
    ]
  },
  {
    "date": "2024-12-25",
    "name": "Christmas",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  },
  {
    "date": "2024-12-26",
    "name": "Boxing Day",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  },
  {
    "date": "2024-12-27",
    "business_day": true,
    "session": {
      "open": "08:00:00",
      "close": "16:30:00"
    },
    "tags": [
      "regular"
    ]
  }
]

Query days in a date range for multiple exchanges

curl "http://localhost:8080/v1/days?start=2024-12-23&end=2024-12-27&mics=XLON&mics=XNYS"

Returns a list grouped by date, where each element contains data for all requested exchanges.

[
  {
    "XLON": {
      "date": "2024-12-23",
      "business_day": true,
      "session": {
        "open": "08:00:00",
        "close": "16:30:00"
      },
      "tags": [
        "regular"
      ]
    },
    "XNYS": {
      "date": "2024-12-23",
      "business_day": true,
      "session": {
        "open": "09:30:00",
        "close": "16:00:00"
      },
      "tags": [
        "regular"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-24",
      "name": "Christmas Eve",
      "business_day": true,
      "session": {
        "open": "08:00:00",
        "close": "12:30:00"
      },
      "tags": [
        "special close"
      ]
    },
    "XNYS": {
      "date": "2024-12-24",
      "name": "Christmas Eve",
      "business_day": true,
      "session": {
        "open": "09:30:00",
        "close": "13:00:00"
      },
      "tags": [
        "special close"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-25",
      "name": "Christmas",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    },
    "XNYS": {
      "date": "2024-12-25",
      "name": "Christmas",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-26",
      "name": "Boxing Day",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    },
    "XNYS": {
      "date": "2024-12-26",
      "business_day": true,
      "session": {
        "open": "09:30:00",
        "close": "16:00:00"
      },
      "tags": [
        "regular"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-27",
      "business_day": true,
      "session": {
        "open": "08:00:00",
        "close": "16:30:00"
      },
      "tags": [
        "regular"
      ]
    },
    "XNYS": {
      "date": "2024-12-27",
      "business_day": true,
      "session": {
        "open": "09:30:00",
        "close": "16:00:00"
      },
      "tags": [
        "regular"
      ]
    }
  }
]

Configuration

The service can be configured via an .env file and/or environment variables. Environment variables must use the prefix EXCHANGE_CALENDAR_SERVICE__ to map to the correct setting.

Here's an example .env file:

exchanges='["XLON", "XNYS"]'  # Limit to these exchanges.
init=customize:init  # Set to a callable to customize calendars on startup. Format: `module:callable`.

Environment variables to the same effect:

export EXCHANGE_CALENDAR_SERVICE_EXCHANGES='["XLON", "XNYS"]'
export EXCHANGE_CALENDAR_SERVICE_INIT="customize:init"

Limiting the supported exchanges

By default, the service will support all available exchanges. In some situations, it may be convenient to limit the supported exchanges to a subset of the available exchanges. Particularly, limiting the number of exchanges improves the startup time of the service. This is because exchange_calendars initializes session data on creation of each exchange calendar. This data is not exposed via this service, but instantiating a lot of calendars can take a noticeable amount of time.

Customization

The service support customizations by executing custom code at startup time.

Init via Environment Variable

Set EXCHANGE_CALENDAR_SERVICE_INIT to a module path pointing to a callable, in the format module:callable. The callable must accept one argument (Settings). On startup, the service will import the callable and invoke it with the settings object as the single argument. This can be used to apply any customizations to the calendars, e.g. adding new calendars, removing existing calendars, registering calendar aliases, et cetera.

For example, setting EXCHANGE_CALENDAR_SERVICE_INIT="customize:init" will execute the init function from the customize module. See the example for details on how calendars can be customized.

export EXCHANGE_CALENDAR_SERVICE_INIT="customize:init"
uv run python -m exchange_calendar_service.app

Via Entrypoints

Custom code can also be discovered automatically via entry points in the exchange_calendar_service.init group. All discovered entrypoints are called sequentially, but in no particular order.

To register an entrypoint, add to your pyproject.toml:

[project.entry-points."exchange_calendar_service.init"]
my_customizer = "my_package:init_function"

Multiple packages can register entrypoints, and all will be called. This allows customization via installed dependencies without needing to set environment variables.

API Reference

Response Model

The response JSON Schema for a single day on a single exchange looks like this:

{
  "$defs": {
    "BusinessDay": {
      "properties": {
        "date": {
          "format": "date",
          "title": "The date of the day in ISO format (YYYY-MM-DD).",
          "type": "string"
        },
        "name": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "The name of the day."
        },
        "tags": {
          "items": {
            "$ref": "#/$defs/Tags"
          },
          "title": "A set of tags associated with the day.",
          "type": "array",
          "uniqueItems": true
        },
        "business_day": {
          "const": true,
          "default": true,
          "title": "Indicates that the day is a business day.",
          "type": "boolean"
        },
        "session": {
          "$ref": "#/$defs/Session",
          "title": "The trading session."
        }
      },
      "required": [
        "date",
        "tags",
        "session"
      ],
      "title": "BusinessDay",
      "type": "object"
    },
    "NonBusinessDay": {
      "properties": {
        "date": {
          "format": "date",
          "title": "The date of the day in ISO format (YYYY-MM-DD).",
          "type": "string"
        },
        "name": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "The name of the day."
        },
        "tags": {
          "items": {
            "$ref": "#/$defs/Tags"
          },
          "title": "A set of tags associated with the day.",
          "type": "array",
          "uniqueItems": true
        },
        "business_day": {
          "const": false,
          "default": false,
          "title": "Indicates that the day is not a business day.",
          "type": "boolean"
        }
      },
      "required": [
        "date",
        "tags"
      ],
      "title": "NonBusinessDay",
      "type": "object"
    },
    "Session": {
      "properties": {
        "open": {
          "format": "time",
          "title": "The start of the trading session (HH:MM:SS).",
          "type": "string"
        },
        "close": {
          "format": "time",
          "title": "The end of the trading session (HH:MM:SS).",
          "type": "string"
        }
      },
      "required": [
        "open",
        "close"
      ],
      "title": "Session",
      "type": "object"
    },
    "Tags": {
      "enum": [
        "special open",
        "special close",
        "quarterly expiry",
        "monthly expiry",
        "month end",
        "holiday",
        "weekend",
        "regular"
      ],
      "title": "Tags",
      "type": "string"
    }
  },
  "discriminator": {
    "mapping": {
      "False": "#/$defs/NonBusinessDay",
      "True": "#/$defs/BusinessDay"
    },
    "propertyName": "business_day"
  },
  "oneOf": [
    {
      "$ref": "#/$defs/BusinessDay"
    },
    {
      "$ref": "#/$defs/NonBusinessDay"
    }
  ]
}

The fields date, business_day and tags are always present:

  • date: The date in ISO format.
  • business_day: Whether the day is a business day or not.
  • tags: A list of tags associated with the day.

The response may optionally provide a name field, e.g. for holidays or special days.

If the day is a business day, the response contains the session field which provides the start and end time of the trading session.

Note: Session open and close times are always in the exchange's timezone.

Tags

While the business_day partitions the days into business and non-business days, tags allow to attach more fine-grained information to individual days. Each day can carry multiple tags, e.g. "holiday" and "weekend". The meaning of the tags is as follows:

  • special open: The trading session starts at a non-standard time, typically later than usual.
  • special close: The trading session ends at a non-standard time, typically earlier than usual.
  • quarterly expiry: Indicates quarterly expiry days, typically the third Thursday in March, June, September and December.
  • monthly expiry: Indicates monthly expiry days, typically the third Thursday in the other months.
  • month end: The last trading day in the respective month.
  • holiday: A holiday on which the exchange is closed.
  • weekend: A weekend day on which the exchange is regularly closed.
  • regular: The day has regular trading session times.

Examples

A regular trading day:

{
  "date": "2026-01-08",
  "business_day": true,
  "session": {
    "open": "08:00:00",
    "close": "16:30:00"
  },
  "tags": [
    "regular"
  ]
}

A regular weekend day:

{
  "date": "2026-01-10",
  "business_day": false,
  "tags": [
    "weekend"
  ]
}

A holiday that would otherwise be a business day:

{
  "date": "2026-01-01",
  "name": "New Year's Day",
  "business_day": false,
  "tags": [
    "holiday"
  ]
}

A holiday that is also a wekend day:

{
  "date": "2022-12-25",
  "name": "Christmas",
  "tags": [
    "weekend",
    "holiday"
  ],
  "business_day": false
}

A special close day that is also the last trading day of a month:

{
  "date": "2022-12-30",
  "name": "New Year's Eve",
  "business_day": true,
  "session": {
    "open": "08:00:00",
    "close": "12:30:00"
  },
  "tags": [
    "special close",
    "month end"
  ]
}

API versioning

There is currently only one version of the API. All endpoints are under /v1/.

Reference Endpoints

These endpoints return reference data for the supported exchanges.

GET /exchanges

Get a list of supported exchanges' MIC codes.

Example request:

curl http: //localhost:8080/v1/exchanges

Response:

[
  "XAMS",
  "XLON",
  "XNYS",
  "XSWX"
]

GET /exchanges/{mic}

Get information about a specific exchange.

Path parameters:

  • mic - MIC code of the exchange

Example request:

curl http://localhost:8080/v1/exchanges/XLON

Response:

{
  "mic": "XLON",
  "tz": "Europe/London"
}

Single Exchange Endpoints

These endpoints return information about one or more days for a single exchange.

GET /exchanges/{mic}/days/{day}

Describe a single day for an exchange.

Path parameters:

  • mic - MIC code of the exchange
  • day - Date in ISO format (e.g., 2024-12-25)

Example request:

curl "http://localhost:8080/v1/exchanges/XLON/days/2024-12-25"

Response:

{
  "date": "2024-12-25",
  "name": "Christmas Day",
  "business_day": false,
  "tags": [
    "holiday"
  ]
}

GET /exchanges/{mic}/days

Get days in a date range that match criteria.

Path Parameters:

  • mic - MIC code of the exchange

Query Parameters:

  • start (required) - Start date in ISO format (inclusive)
  • end (required) - End date in ISO format (inclusive)
  • business_day (optional) - Filter to only business days (true) or non-business days (false)
  • include_tags (optional, repeatable) - Only include days with all the given tags
  • exclude_tags (optional, repeatable) - Exclude days with any of the given tags
  • order (optional, default: asc) - Sort order: asc or desc
  • limit (optional) - Maximum number of days to return

Example request:

curl "http://localhost:8080/v1/exchanges/XLON/days?start=2024-12-24&end=2024-12-27&business_day=false"

Response:

[
  {
    "date": "2024-12-25",
    "name": "Christmas",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  },
  {
    "date": "2024-12-26",
    "name": "Boxing Day",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  }
]

Example request:

curl "http://localhost:8080/v1/exchanges/XLON/days?start=2024-12-24&end=2024-12-31&include_tags=special%20close&include_tags=month%20end&order=asc"

Response:

[
  {
    "date": "2024-12-31",
    "name": "New Year's Eve",
    "business_day": true,
    "session": {
      "open": "08:00:00",
      "close": "12:30:00"
    },
    "tags": [
      "special close",
      "month end"
    ]
  }
]

GET /exchanges/{mic}/days/{day}/next

Get the next (or previous) days matching criteria relative to a reference day.

Path Parameters:

  • mic - MIC code of the exchange
  • day - Reference date in ISO format

Query Parameters:

  • direction (optional, default: forward) - Search direction: forward or backward
  • inclusive (optional, default: true) - Include the reference day if it matches
  • end (optional) - End date to bound the search (inclusive)
  • business_day (optional) - Filter to only business days or non-business days
  • include_tags (optional, repeatable) - Only include days with all the given tags
  • exclude_tags (optional, repeatable) - Exclude days with any of the given tags
  • limit (optional) - Maximum number of days to return
  • order (optional, default: asc) - Sort order of results: asc or desc

Example:

curl "http://localhost:8080/v1/exchanges/XLON/days/2024-12-20/next?direction=forward&limit=3&business_day=false"
[
  {
    "date": "2024-12-21",
    "business_day": false,
    "tags": [
      "weekend"
    ]
  },
  {
    "date": "2024-12-22",
    "business_day": false,
    "tags": [
      "weekend"
    ]
  },
  {
    "date": "2024-12-25",
    "name": "Christmas",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  }
]

Multi-Exchange Endpoints

These endpoints return information about one or more days for multiple exchanges in a single request.

GET /days/{day}

Get a specific day for multiple exchanges.

Path parameters:

  • day - Date in ISO format (e.g., 2024-12-25)

Query parameters:

  • mics (required, repeatable) - One or more MIC codes of the exchanges to query

Example request:

curl "http://localhost:8080/v1/days/2024-12-25?mics=XLON&mics=XSWX"

Response:

{
  "XLON": {
    "date": "2024-12-25",
    "name": "Christmas",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  },
  "XSWX": {
    "date": "2024-12-25",
    "name": "Christmas",
    "business_day": false,
    "tags": [
      "holiday"
    ]
  }
}

GET /days

Get days in a date range that match criteria for multiple exchanges.

Query Parameters:

  • mics (required, repeatable) - One or more MIC codes of the exchanges to query
  • start (required) - Start date in ISO format (inclusive)
  • end (required) - End date in ISO format (inclusive)
  • business_day (optional) - Filter to only business days (true) or non-business days (false)
  • include_tags (optional, repeatable) - Only include days with all the given tags
  • exclude_tags (optional, repeatable) - Exclude days with any of the given tags
  • order (optional, default: asc) - Sort order: asc or desc
  • limit (optional) - Maximum number of date records to return (each record contains all MICs' data for that date)

The response is grouped by date, with MICs within each date ordered alphabetically.

Example request:

curl "http://localhost:8080/v1/days?start=2024-12-24&end=2024-12-27&mics=XLON&mics=XNYS&business_day=false"

Response:

[
  {
    "XLON": {
      "date": "2024-12-25",
      "name": "Christmas",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    },
    "XNYS": {
      "date": "2024-12-25",
      "name": "Christmas",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-26",
      "name": "Boxing Day",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    }
  }
]

Note: The limit parameter applies to the number of date records returned, not the total number of individual exchange-day entries.

GET /days/{day}/next

Get the next days matching criteria relative to a day for multiple exchanges.

Path parameters:

  • day - Date in ISO format (e.g., 2024-12-25)

Query parameters:

  • mics (required, repeatable) - One or more MIC codes of the exchanges to query
  • direction (optional, default: forward) - Search direction: forward or backward
  • inclusive (optional, default: true) - If true, include the start day in results
  • end (optional) - End date to limit the search range
  • business_day (optional) - Filter to only business days (true) or non-business days (false)
  • include_tags (optional, repeatable) - Only include days with all the given tags
  • exclude_tags (optional, repeatable) - Exclude days with any of the given tags
  • order (optional, default: asc) - Sort order: asc or desc
  • limit (optional) - Maximum number of date records to return

The response is grouped by date, with MICs within each date ordered alphabetically.

Example request:

curl "http://localhost:8080/v1/days/2024-12-24/next?mics=XLON&mics=XSWX&direction=forward&limit=3"

Response:

[
  {
    "XLON": {
      "date": "2024-12-24",
      "name": "Christmas Eve",
      "business_day": true,
      "session": {
        "open": "08:00:00",
        "close": "12:30:00"
      },
      "tags": [
        "special close"
      ]
    },
    "XSWX": {
      "date": "2024-12-24",
      "name": "Christmas Eve",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-25",
      "name": "Christmas",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    },
    "XSWX": {
      "date": "2024-12-25",
      "name": "Christmas",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    }
  },
  {
    "XLON": {
      "date": "2024-12-26",
      "name": "Boxing Day",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    },
    "XSWX": {
      "date": "2024-12-26",
      "name": "Boxing Day",
      "business_day": false,
      "tags": [
        "holiday"
      ]
    }
  }
]

Development

Clone this repository and run uv sync and you are good to go.

Testing

Run the full test suite with coverage:

uv run pytest -v tests/ --cov=exchange_calendar_service

Building the Docker Image

From the project root:

docker build -f docker/Dockerfile -t exchange-calendar-service .

Running the Container

docker run -p 8080:8080 exchange-calendar-service

License

Apache-2.0

About

A simple HTTP-based web service to query exchange calendars.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors