An simple HTTP service for querying exchange calendars for stock exchanges. Built on top of exchange_calendars and exchange_calendars_extensions.
- 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.
The package requires Python 3.11 or later.
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-serviceThis 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-serviceAdd the PyPI package as a dependency to your Python project via uv:
uv add exchange-calendar-serviceOr 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-serviceor by running the Python module directly:
python -m exchange_calendar_serviceFor 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:latestAssuming 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.
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"
]curl "http://localhost:8080/v1/exchanges/XLON"returns Information about the London Stock Exchange.
{
"mic": "XLON",
"tz": "Europe/London"
}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"
]
}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"
]
}
]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"
]
}
}
]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"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.
The service support customizations by executing custom code at startup time.
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.appCustom 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.
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.
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.
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"
]
}There is currently only one version of the API. All endpoints are under /v1/.
These endpoints return reference data for the supported exchanges.
Get a list of supported exchanges' MIC codes.
Example request:
curl http: //localhost:8080/v1/exchangesResponse:
[
"XAMS",
"XLON",
"XNYS",
"XSWX"
]Get information about a specific exchange.
Path parameters:
mic- MIC code of the exchange
Example request:
curl http://localhost:8080/v1/exchanges/XLONResponse:
{
"mic": "XLON",
"tz": "Europe/London"
}These endpoints return information about one or more days for a single exchange.
Describe a single day for an exchange.
Path parameters:
mic- MIC code of the exchangeday- 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 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 tagsexclude_tags(optional, repeatable) - Exclude days with any of the given tagsorder(optional, default:asc) - Sort order:ascordesclimit(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 the next (or previous) days matching criteria relative to a reference day.
Path Parameters:
mic- MIC code of the exchangeday- Reference date in ISO format
Query Parameters:
direction(optional, default:forward) - Search direction:forwardorbackwardinclusive(optional, default:true) - Include the reference day if it matchesend(optional) - End date to bound the search (inclusive)business_day(optional) - Filter to only business days or non-business daysinclude_tags(optional, repeatable) - Only include days with all the given tagsexclude_tags(optional, repeatable) - Exclude days with any of the given tagslimit(optional) - Maximum number of days to returnorder(optional, default:asc) - Sort order of results:ascordesc
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"
]
}
]These endpoints return information about one or more days for multiple exchanges in a single request.
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 in a date range that match criteria for multiple exchanges.
Query Parameters:
mics(required, repeatable) - One or more MIC codes of the exchanges to querystart(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 tagsexclude_tags(optional, repeatable) - Exclude days with any of the given tagsorder(optional, default:asc) - Sort order:ascordesclimit(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 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 querydirection(optional, default:forward) - Search direction:forwardorbackwardinclusive(optional, default:true) - Iftrue, include the start day in resultsend(optional) - End date to limit the search rangebusiness_day(optional) - Filter to only business days (true) or non-business days (false)include_tags(optional, repeatable) - Only include days with all the given tagsexclude_tags(optional, repeatable) - Exclude days with any of the given tagsorder(optional, default:asc) - Sort order:ascordesclimit(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"
]
}
}
]Clone this repository and run uv sync and you are good to go.
Run the full test suite with coverage:
uv run pytest -v tests/ --cov=exchange_calendar_serviceFrom the project root:
docker build -f docker/Dockerfile -t exchange-calendar-service .docker run -p 8080:8080 exchange-calendar-serviceApache-2.0