Calendarios: XNYS, XMAD y FX¶
Un precio sin su calendario es una cifra huérfana. El calendario decide qué días hay
mercado, a qué hora abre y cierra, qué pasa con el cambio de hora y cuándo una ausencia es un
festivo y cuándo es un fallo del proveedor. En el banco de pruebas el calendario no es un
detalle de presentación: entra en la limpieza, en el remuestreo, en las primitivas de sesión y
en el data_hash.
Qué vas a aprender¶
- Qué es un calendario versionado (
XNYS-1999-2026.v1) y por qué su huella entra en el hash. - Cómo se tratan la sesión regular, el DST, los festivos y los cierres anticipados.
- Por qué el cierre anticipado de NYSE se fija en 13:01 y no en 13:00.
- Cómo se construyen las rejillas de minutos y los cubos de remuestreo.
- Qué es una «sesión» en FX 24/5 y cómo se corrigió la fecha de Yahoo.
- Cómo lo hace la capa de datos de la plataforma con
finaz_calendarsyCalendarSnapshot(Q020–Q022).
1. La idea: el calendario es un dato versionado¶
flowchart LR
CAL[CalendarSpec<br/>calendar_id + versión + tz IANA] --> F[fingerprint<br/>versión:sha256 corto]
F --> MAN[manifest.json<br/>calendar_version]
MAN --> H[data_hash]
CAL --> CL[limpieza: fuera de sesión]
CAL --> RS[remuestreo: ancla de sesión]
CAL --> SP[primitivas de sesión:<br/>opening_range, session_vwap…]
Un calendario es una tabla explícita (festivos, cierres anticipados, horario regular, zona
IANA) con una versión y una huella. Si cambia un festivo, cambia la huella; si cambia la huella,
cambia el data_hash. Así nadie puede comparar dos corridas hechas con calendarios distintos
creyendo que son la misma.
| Calendario | Módulo | Versión y huella | Zona |
|---|---|---|---|
| XNYS (NYSE/Nasdaq) | finazbench/data/calendar.py |
XNYS-1999-2026.v1:4f047091cf176d61 |
America/New_York |
| XMAD (BME) | finazbench/data/calendar_xmad.py |
XMAD-2015-2026.v1:0ef6ac9ad2914e1d |
Europe/Madrid |
| FX (spot 24/5) | finazbench/data/calendar_fx.py |
FX-2015-2026.v1:0141baa75b4393bc |
Europe/London |
Registro por calendar_id
calendar.py expone register_calendar y get_calendar. Cada CalendarSpec declara cuatro
operaciones (session_id_for, is_regular_session, session_bounds, sessions_between) y
su fingerprint. Añadir un mercado es escribir su módulo y registrarlo, sin tocar el
pipeline. Pedir un id no registrado lanza: aplicar el calendario de Nueva York a BME
desplazaría las sesiones seis horas sin que nada fallase visiblemente.
2. XNYS: la bolsa de Nueva York¶
2.1 Sesión regular y DST¶
- Sesión regular 09:30–16:00 hora de Nueva York. Última barra de 1 minuto:
[15:59, 16:00). - DST real vía
zoneinfo: la apertura son las 14:30 UTC en invierno y las 13:30 UTC en verano; la sesión dura 6,5 h económicas en ambos casos.
El mismo 09:30, dos instantes UTC
| Sesión | Hora local | UTC |
|---|---|---|
| 2025-01-15 (EST) | 09:30 | 14:30Z |
| 2025-07-16 (EDT) | 09:30 | 13:30Z |
| Un sistema que guardara «14:30 UTC» fijo abriría una hora tarde medio año. |
2.2 Festivos y cierres extraordinarios¶
Tabla explícita, incluidos los cierres que ninguna regla genera: 11–14/09/2001, 02/01/2007, 29–30/10/2012, 05/12/2018 y 09/01/2025.
Validación contra el dato, no contra una web: los 63 días hábiles sin barras de NVDA en 2020-03-24…2026-09-14 coinciden exactamente con la tabla de festivos. Ni uno más, ni uno menos.
2.3 Cierres anticipados: ¿por qué 13:01?¶
El cierre es el extremo abierto del intervalo. En sesión regular vale 16:00 porque la última
barra es [15:59, 16:00). En sesión corta, la última barra que emite el proveedor empieza a las
13:00 y es la subasta de cierre: NVDA el 29/11/2024 movió 26,4 millones de acciones en ese
minuto, trece veces un minuto normal. Poner el cierre en 13:00 dejaría fuera justo la barra que
lleva el precio de cierre oficial.
flowchart LR
A["12:58–12:59"] --> B["12:59–13:00"] --> C["13:00–13:01<br/>subasta de cierre"] -.->|fuera de sesión| D["13:01–15:59<br/>relleno de volumen 0<br/>del proveedor"]
Las barras de relleno posteriores (179 por sesión corta en 2021-11-26 y 2023-07-03) quedan
fuera de sesión, is_tradable=False, y no se opera sobre ellas.
3. XMAD: el mercado continuo español¶
- Mercado continuo 09:00–17:30 hora de Madrid.
- Medias sesiones el 24 y 31 de diciembre con cierre a las 14:00.
Validación: los 59 días hábiles sin barra comunes a BBVA.MC, SAN.MC y TEF.MC coinciden exactamente con la tabla de festivos, con dos excepciones que no se metieron en la tabla porque no son festivos:
| Fecha | Qué pasa | Tratamiento |
|---|---|---|
| 2018-09-20 | BME operó y Yahoo no entrega barra para ninguno de los tres | Hueco del proveedor (DEV-01B-03) |
| 2026-09-07 | Falta en BBVA.MC y TEF.MC, pero no en SAN.MC | Hueco parcial |
| 2019-12-25 | Los tres CSV traen una barra con O=H=L=C y volumen 0 en festivo |
El calendario la deja fuera de sesión |
Lo que el dato diario no puede confirmar (DEV-01B-04)
El cierre a las 14:00 de las medias sesiones sale de la norma publicada de BME, no del dato: una barra diaria no lleva hora. El dato sí muestra volumen anormalmente bajo esos días (mediana del cociente contra la anual: 0,31), compatible pero no demostrativo.
4. FX: un mercado sin apertura¶
El spot FX es OTC y continuo desde el domingo por la tarde hasta el viernes por la tarde en Nueva York. No hay apertura ni cierre económicos: hay una convención de corte diario que elige el proveedor. Lo honesto es declarar la del nuestro.
meta.exchangeTimezoneNamede los pares en Yahoo:Europe/London.- Las barras diarias se sellan a la medianoche de Londres.
- Sesión FX del día D:
[D 00:00, D+1 00:00)enEurope/London.
Todas las sesiones duran 24 h exactas, y no por casualidad: el Reino Unido cambia la hora en domingo, y el domingo no es sesión.
4.1 El caso de las fechas corridas¶
sequenceDiagram
participant Y as Yahoo (sello real)
participant S as Script del host
participant C as CSV
Y->>S: 1427670000 = 2015-03-29T23:00Z<br/>(= lunes 30, 00:00 BST)
S->>C: fromtimestamp(t, UTC) → "2015-03-29"<br/>(¡domingo!)
Note over C: En horario BST cada fila<br/>sale un día antes
C->>C: calendar_fx.session_day_of_utc_date<br/>re-fecha → 2015-03-30
La política (DEV-01B-01) es re-fechar, no descartar. Descartar las filas de domingo habría tirado el 12 % de cada serie y dejado el resto desplazado un día durante medio año: un look-ahead silencioso de 24 horas.
El desempate del domingo del cambio de hora
El domingo en que arranca el BST, los dos candidatos de fecha encajan con la fila. Se elige el más tardío, porque el sello real de la fila etiquetada 2015-03-29 es la medianoche del lunes 30. Elegir el domingo produciría doce barras adelantadas 24 h.
5. Rejillas de minutos y remuestreo¶
Una rejilla es la lista de inicios de barra que el calendario predice para una sesión. Se usa para tres cosas:
- Detectar huecos: una barra que la rejilla predice y el dato no trae es un hueco (se cuenta).
- Anclar cubos: un cubo de 1 h empieza en la apertura de sesión (09:30), no a medianoche (09:00) ni en la primera barra presente.
- Primitivas de sesión:
is_penultimate_regular_barsale de la rejilla determinista, nunca de mirar la barra siguiente del array (así batch y online coinciden).
La 1h de NYSE tiene una última barra parcial
Con apertura a las 09:30 y cierre a las 16:00, la sexta hora es [15:30, 16:00): media hora.
Por eso P12 en 1h solo admite range_minutes = 60 y la matriz declara UNSUPPORTED los
defaults que no encajan.
6. En la capa de datos de la plataforma: finaz_calendars¶
La plataforma (paquete AG-007, packages/finaz_calendars/) reescribe la idea con contratos más estrictos.
| Concepto | Banco de backtesting (finazbench/data/) |
Plataforma (finaz_calendars) |
|---|---|---|
| Identidad | calendar_id + versión + huella corta |
CalendarSnapshot: versión + tz IANA + fuente + lista explícita de sesiones; identidad = hash canónico FINAZ_JSON_V1 |
| Día sin fila | festivo implícito si no es día hábil de la tabla | anomalía (REFERENCE_NOT_FOUND); los festivos llevan fila propia con is_trading_day=False |
| Disponibilidad | data_available_ns = market_close_ns |
disponibilidad_ns(close) = close + 1 s (fixture sim.01) |
| Rejilla | derivada de session_bounds |
minutos_negociacion(sesion): minutos [open, close) completos, en ns UTC |
| Proveedores externos | — | exchange_calendars y QuantLib reservados: lanzan UNSUPPORTED_MODE |
Las pruebas de aceptación del pack que cubre son:
| ID | Qué exige | Dónde |
|---|---|---|
| Q020 | DST NYSE/Europa y desfase: UTC correcto desde un fixture versionado | finaz_clock, clock/test_dst.py |
| Q021 | Festivos, cierre anticipado y pausas: no acumular tiempo fuera de intervalos; el resto final no genera barra parcial | finaz_calendars/grids.py, finaz_clock |
| Q022 | Overnight de futuros, fin de semana FX, cripto: resets y calendarios propios | clock/test_multi_market.py |
Estado declarado
docs/v2/MOTORES.md registra AG-007 con 49 tests (DST Nueva York/Madrid, early close, overnight
Chicago). En la ingesta de datos reales del 2026-09-22 el calendar_hash del snapshot es un
hash declarativo de los calendarios del banco de backtesting (finazbench/data/): todavía no hay un CalendarSnapshot real
registrado en PostgreSQL (ver el capítulo siguiente).
# finaz_calendars: una sesión es un dato validado, sin coerciones silenciosas
from finaz_calendars.snapshot import CalendarSnapshot, crear_sesion_local
from finaz_calendars.grids import minutos_negociacion
s = crear_sesion_local("2024-11-29", "09:30", "13:01", "America/New_York")
print(len(minutos_negociacion(s))) # minutos completos dentro de [open, close)
Formato de las horas
La firma es crear_sesion_local(fecha, apertura, cierre, tz, es_dia_negociable=True); comprueba el formato de hora aceptado en
packages/finaz_calendars/snapshot.py antes de copiarlo.
Resumen¶
- El calendario es un dato versionado con huella que entra en el
data_hash. - XNYS: 09:30–16:00 NY, DST real, festivos explícitos validados contra el dato, cierre corto a 13:01 para no perder la subasta.
- XMAD: 09:00–17:30 Madrid, medias sesiones a las 14:00 (norma, no dato); los huecos del proveedor no se convierten en festivos.
- FX: sesión
[00:00, 24:00)Londres; las fechas de Yahoo se re-fechan para no crear un look-ahead de 24 h. finaz_calendarshace explícitas las sesiones (CalendarSnapshot), trata la fila ausente como anomalía y cubre Q020–Q022.
Para practicar¶
- Calcula en UTC la apertura y el cierre de NYSE el 2024-07-03 (sesión corta, EDT) y el 2024-11-29 (sesión corta, EST).
- ¿Por qué convertir el 2018-09-20 en festivo de BME sería «una afirmación falsa sobre el mercado»? Redáctalo en dos frases.
- Dibuja la rejilla de cubos de 1 h de una sesión regular de NYSE y marca la barra parcial.
- Con
finaz_calendars, ¿qué código de error devolvería un calendario al preguntar por una fecha sin fila? ¿Por qué es mejor que asumir festivo?