Saltar a contenido

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_calendars y CalendarSnapshot (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.exchangeTimezoneName de 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) en Europe/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:

  1. Detectar huecos: una barra que la rejilla predice y el dato no trae es un hueco (se cuenta).
  2. 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.
  3. Primitivas de sesión: is_penultimate_regular_bar sale 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_calendars hace explícitas las sesiones (CalendarSnapshot), trata la fila ausente como anomalía y cubre Q020–Q022.

Para practicar

  1. 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).
  2. ¿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.
  3. Dibuja la rejilla de cubos de 1 h de una sesión regular de NYSE y marca la barra parcial.
  4. 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?