A simple HTTP-based web service to query exchange calendars.
Project description
Exchange Calendar Service
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 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 /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 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 /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 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"
]
}
]
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 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 /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 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"
]
}
}
]
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file exchange_calendar_service-0.2.0.tar.gz.
File metadata
- Download URL: exchange_calendar_service-0.2.0.tar.gz
- Upload date:
- Size: 116.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25b77457f3c0fa6e42669c135945aa4eb9d18951c0938150239044080b80a022
|
|
| MD5 |
5ef6fa918be1e8f32f94f9c3b79973f4
|
|
| BLAKE2b-256 |
d114617768fd3fbafc99cefd504d75f61e7015296752cf26b8259a249c64509d
|
Provenance
The following attestation bundles were made for exchange_calendar_service-0.2.0.tar.gz:
Publisher:
release.yml on jenskeiner/exchange_calendar_service
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
exchange_calendar_service-0.2.0.tar.gz -
Subject digest:
25b77457f3c0fa6e42669c135945aa4eb9d18951c0938150239044080b80a022 - Sigstore transparency entry: 969504166
- Sigstore integration time:
-
Permalink:
jenskeiner/exchange_calendar_service@c00e1b46fa8c837c944b66a8f39c930eb0be1e23 -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/jenskeiner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00e1b46fa8c837c944b66a8f39c930eb0be1e23 -
Trigger Event:
release
-
Statement type:
File details
Details for the file exchange_calendar_service-0.2.0-py3-none-any.whl.
File metadata
- Download URL: exchange_calendar_service-0.2.0-py3-none-any.whl
- Upload date:
- Size: 27.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e208d04d1c753f3f26b08ea55c8f557ddae99e9ee6340caeb67b2af6f93986b
|
|
| MD5 |
749e3ac14a4dbf4edbd692bd8a96bf33
|
|
| BLAKE2b-256 |
56ac4328037006a90ac4c140d1f7305e8fe0c0e581477568be9941da75024b81
|
Provenance
The following attestation bundles were made for exchange_calendar_service-0.2.0-py3-none-any.whl:
Publisher:
release.yml on jenskeiner/exchange_calendar_service
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
exchange_calendar_service-0.2.0-py3-none-any.whl -
Subject digest:
1e208d04d1c753f3f26b08ea55c8f557ddae99e9ee6340caeb67b2af6f93986b - Sigstore transparency entry: 969504168
- Sigstore integration time:
-
Permalink:
jenskeiner/exchange_calendar_service@c00e1b46fa8c837c944b66a8f39c930eb0be1e23 -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/jenskeiner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c00e1b46fa8c837c944b66a8f39c930eb0be1e23 -
Trigger Event:
release
-
Statement type: