Business Days APIDocumentation

API des jours ouvrables.

Prochain jour ouvrable, comptage entre deux dates, listing mensuel, et CalSpan — les bornes ouvrables enchaînables pour vos pipelines ETL et reporting financier au Maroc.

Par Marouane FAKIR··Lecture ~8 min

Présentation

Calculer le prochain jour ouvrable semble trivial — jusqu'à ce qu'un vendredi de décrampon tombe un Aid Al Adha, ou qu'un lundi de reporting tombe sur une fête nationale décrétée deux jours plus tôt. Ce type d'erreur silencieuse dans un pipeline ETL produit des trous dans les séries chronologiques, des VNI calculées sur des dates incorrectes, ou des orchestrateurs qui tournent à vide.

L'API des jours ouvrables de Calendar API centralise cette logique et expose des opérations métier directement exploitables : prochain/précédent jour ouvrable, comptage, listing, et le concept de CalSpan pour les bornes de périodes.

Définition d'un jour ouvrable

Dans le contexte de Calendar API pour le Maroc, un jour ouvrable est un jour qui remplit toutes les conditions suivantes :

  • N'est pas un samedi ni un dimanche
  • N'est pas un jour férié National, Religious ou Exceptional au statut Official
⚠️ Fêtes au statut Estimated. Tant qu'une fête religieuse est encore Estimated, les jours ouvrables sont calculés en supposant qu'elle sera confirmée à la date estimée. Consultez le guide jours fériés pour le pattern de polling.

Installation

terminal
pip install pycalendar-api
client.py
from pycalendar_api import CalendarApi, to_date

api = CalendarApi.from_env()  ## export PYCALENDAR_APIKEY = YOUR_API_KEY
# ou : CalendarApi('YOUR_KEY')

Prochain / précédent jour ouvrable

Ces deux opérations sont le cœur de l'API pour les cas d'usage de type settlement, valeur liquidative ou date d'effet : trouver le prochain ou le précédent jour où les marchés et institutions sont ouverts.

next_prev.py
from pycalendar_api import CalendarApi, to_date
from datetime import date

api = CalendarApi.from_env()

# Lundi 13 jan 2025 → mardi 15 (le 14 est Nouvel An Amazigh)
nxt = api.bdays.next_date(to_date('13/01/2025'))
print(nxt)
# NextDate(date=datetime.date(2025, 1, 13),
#          next_date=datetime.date(2025, 1, 15))

# Mercredi 15 jan → lundi 13 (le 14 est férié)
prev = api.bdays.previous_date(to_date('15/01/2025'))
print(prev)
# PreviousDate(date=datetime.date(2025, 1, 15),
#              previous_date=datetime.date(2025, 1, 13))

# Utiliser date.today() directement
nxt_today = api.bdays.next_date(date.today())

Comptage entre deux dates

Retourne le nombre de jours ouvrables dans un intervalle [start, end] inclusif. Utile pour les calculs de délais contractuels, de périodes de préavis, ou pour valider la complétude d'une série chronologique.

count.py
count = api.bdays.count(
    to_date('02/01/2025'),
    to_date('31/12/2025')
)
print(count)
# DaysCount(start=2025-01-02, end=2025-12-31, count=247)

# Vérifier l'intégrité d'une série chronologique
import polars as pl

df = pl.read_parquet('data_2025.parquet')
expected = api.bdays.count(df['market_date'].min(), df['market_date'].max()).count
actual = df['date_marche'].unique().len()

if actual != expected:
    raise ValueError(f"Série incomplète : {actual} observations, attendu {expected}")

Listing mensuel / annuel

Retourne un DateSeries contenant l'ensemble des jours ouvrables d'un mois ou d'une année entière, sous forme de SortedSet[date].

list_bdays.py
# Mois de juin 2025
june = api.bdays.bdays_of(2025, month=6)
print(june)
# DateSeries(nitems=19, serie=SortedSet([date(2025,6,2), ...]))

# Année complète 2025
full_year = api.bdays.bdays_of(2025)
print(full_year.nitems)
# 247

# Conversion en liste Python
bdays_list = list(full_year.serie)

# Alimenter un calendrier Polars
import polars as pl
cal_df = pl.DataFrame({'date': bdays_list})

CalSpan — bornes ouvrables de périodes

CalSpan est le concept le plus distinctif de l'API. Il retourne les bornes ouvrables d'une période — mois, trimestre, semestre ou année — sous la forme [start, end]start est le dernier jour ouvrable de la période précédente et end est le dernier jour ouvrable de la période demandée.

Cette convention est très utilisée dans la finance de marché pour le calcul d'une performance sur une période. Pour un instrument financier, la borne d'ouverture est le cours du dernier jour ouvrable du trimestre précédent, et la borne de fermeture est le cours du dernier jour ouvrable du trimestre en cours. Les règles d'inclusion ou d'exclusion sont laissées à la discretion de l'utilisateur.

  Exemple: Année 2025 — CalSpan par trimestre

  Q1                      Q2                      Q3                      Q4
  |———————————————————————|———————————————————————|———————————————————————|
  s=2024-12-31            s=2025-03-28            s=2025-06-30            s=2025-09-30
  e=2025-03-28            e=2025-06-30            e=2025-09-30            e=2025-12-31

  start = dernier j.ouv. période N-1 | end = dernier j.ouv. période N
  Les spans s'enchaînent sans trou ni chevauchement → BETWEEN :start AND :end
calspan.py
# Année complète
y2025 = api.bdays.span(2025)
# CalSpan(start_date=2024-12-31, end_date=2025-12-31, year=2025, ...)

# Par trimestre (quarter= 1..4)
q1 = api.bdays.span(2025, quarter=1)
# CalSpan(start_date=2024-12-31, end_date=2025-03-28, year=2025, quarter=1, ...)
q2 = api.bdays.span(2025, quarter=2)
# CalSpan(start_date=2025-03-28, end_date=2025-06-30, year=2025, quarter=2, ...)

# Par semestre (semester= 1..2)
s1 = api.bdays.span(2025, semester=1)
# CalSpan(start_date=2024-12-31, end_date=2025-06-30, year=2025, semester=1, ...)

# Par mois (month= 1..12)
jul = api.bdays.span(2025, month=7)
# CalSpan(start_date=2025-06-30, end_date=2025-07-31, year=2025, month=7, ...)

Usage SQL avec CalSpan

Les bornes CalSpan sont directement utilisables dans une clause SQL BETWEEN pour les séries ou par égalité pour les points aux bornes. Les spans étant conçus pour s'enchaîner sans trou ni chevauchement, il n'y a pas de risque de double comptage entre deux périodes.

sql_query.py
from sqlalchemy import text
from pycalendar_api import CalendarApi

api = CalendarApi.from_env()
span = api.bdays.span(2025, quarter=2)
# CalSpan(start_date=2025-03-28, end_date=2025-06-30)

# Requête de performance trimestrielle de l'indice du MASI.20
query = text("""
    SELECT
        s.index_key,
        s.index_value AS start_value,
        e.index_value AS end_value,
        ((e.index_value /NULLIF(s.index_value, 0) - 1) * 100 AS perf_q2
    FROM
        index_table s
    LEFT JOIN
        index_table e ON s.index_id = e.index_id AND
        e.market_date = :end_date
    WHERE
        s.market_date = :start_date AND
        s.index_key = :index_key
""")

with engine.connect() as conn:
    result = conn.execute(query, {
        'start_date': span.start_date,
        'end_date': span.end_date,
        'index_key': 'MASI.20'
    })

Enchaînement de spans

Pour générer tous les spans d'une année en une seule passe — utile pour construire un reporting glissant ou initialiser une table de calendrier.

span_chain.py
# 12 spans mensuels — enchaînés sans trou
monthly_spans = [
    api.bdays.span(2025, month=m)
    for m in range(1, 13)
]

# Vérification : end[n] == start[n+1]
for i in range(len(monthly_spans) - 1):
    assert monthly_spans[i].end == monthly_spans[i + 1].start

# Construire une table de référence Polars
import polars as pl
spans_df = pl.DataFrame([
    {'month': s.month, 'start': s.start_date, 'end': s.end_date} for s in monthly_spans])

Intégration avec les orchestrateurs

Un cas d'usage fréquent : conditionner l'exécution d'un DAG ou d'un job au fait que la date courante soit un jour ouvrable. Sans Calendar API, cela repose sur une liste hardcodée de jours fériés — source de bugs silencieux.

Airflow

dag_market.py
from airflow.sdk import dag, task
from airflow.exceptions import AirflowSkipException
from pycalendar_api import CalendarApi
from datetime import date, timedelta

api = CalendarApi.from_env()

def is_business_day(d: date) -> bool:
    r = api.holidays.is_holiday(d)
    return not r.is_holiday and d.weekday() < 5

@dag(schedule="0 18 * * 1-5", catchup=False)
def daily_index_pipeline():

    @task.short_circuit
    def check_business_day(**ctx):
        run_date = ctx['data_interval_end'].date()
        if not is_business_day(run_date):
            raise AirflowSkipException(f"{run_date} is not a business day")
        return True

    @task
    def collect_market_data(**ctx):
        run_date = ctx['data_interval_end'].date()
        span = api.bdays.span(run_date.year, month=run_date.month)
        # span.start_date / span.end_date → bornes pour les requêtes SQL
        ...

    check_business_day() >> collect_market_data()

Dagster

dagster_job.py
from dagster import sensor, RunRequest, SkipReason, define_asset_job
from pycalendar_api import CalendarApi
from datetime import date

api = CalendarApi.from_env()

collect_job = define_asset_job("collect_job", selection="*")

@sensor(job=collect_job, minimum_interval_seconds=3600)
def business_day_sensor(context):
    today = date.today()
    r = api.holidays.is_holiday(today)
    if r.is_holiday or today.weekday() < 5:
        yield RunRequest(run_key=str(today))
    else:
        yield SkipReason(f"{today} n'est pas un jour ouvrable")

Modèles de réponse

models.py
@dataclass
class NextDate:
    date:      date   #La date passée en paramètre
    next_date: date   # Le prochain jour ouvrable

@dataclass
class PreviousDate:
    date:          date # La date passée en paramètre
    previous_date: date # Le dernier jour ouvrable. (La veille)

@dataclass
class DaysCount:
    start_date: date
    end_date:   date
    count: int

@dataclass
class DateSeries:
    nitems: int            # nombre de jours ouvrables
    min_date: date          # Date min de la série
    max_date: date          # Date max de la série
    serie: SortedSet[date] # ensemble ordonné des dates

@dataclass
class CalSpan:
    start_date: date  # dernier j.ouv. de la période précédente
    end_date:   date  # dernier j.ouv. de la période demandée
    year: int         # L'année de la période
    semester: int     # Le semestre de la période. Si semestre
    quarter: int      # Le trimestre de la période. Si Trimestre
    month: int        # Le mois de la période. Si Mois
📦 Tous les modèles sont des dataclasses standard. Pas de Pydantic, pas de dépendances lourdes. La conversion depuis JSON est gérée en interne par le SDK.

Référence OpenAPI.

Endpoints, paramètres et schémas de réponse — extraits directement du schéma OpenAPI 3.1.

Prêt à intégrer ?

Créez votre compte gratuitement et obtenez une clé API en 30 secondes. Pas de carte bancaire.