2. El calendario de mercado como fuente de verdad
Nivel 9: Orquestación y calendario
Hay una línea de código que aparece en casi todos los sistemas caseros y que está mal en todos ellos: comprobar si hoy es de lunes a viernes para decidir si se opera. Es una aproximación que acierta la mayoría de los días del año, y eso es precisamente lo que la hace peligrosa: falla pocas veces, siempre por sorpresa, y casi siempre en días en los que el mercado está raro.
Por qué el día de la semana no basta
Un calendario bursátil real tiene, como mínimo, cuatro cosas que el día de la semana no sabe:
- Festivos. El mercado estadounidense cierra en torno a nueve o diez días al año, y varios son móviles: el Viernes Santo, el Día de los Caídos o Acción de Gracias caen en fechas distintas cada año.
- Medias sesiones. Días en los que se abre a la hora normal y se cierra a primera hora de la tarde, como el viernes siguiente a Acción de Gracias, que en la bolsa de Nueva York cierra a la una del mediodía hora local en lugar de a las cuatro.
- Subastas y cortes. El precio de cierre no sale de la última operación, sino de una subasta, y las órdenes al cierre tienen un corte de entrada minutos antes: en la bolsa de Nueva York está en torno a las 15:50 hora local. Llegar a las 15:58 con una orden al cierre es llegar tarde aunque el mercado siga abierto.
- Mercados distintos. Si operas a la vez Nueva York, Londres y Fráncfort, no comparten ni festivos ni horas. El 4 de julio Nueva York está cerrada y Europa abierta; el 26 de diciembre pasa lo contrario en varias plazas.
El calendario cambia, y por eso no puede vivir en tu código
El argumento definitivo contra la lista de festivos escrita a mano no es que sea fea, es que caduca. En 2022 la bolsa de Nueva York incorporó el 19 de junio como festivo bursátil: cualquier sistema con la lista congelada del año anterior intentó operar ese día contra un mercado cerrado. También cambian los horarios de las subastas, se añaden cierres extraordinarios por duelo nacional y se ajustan las medias sesiones cuando Nochebuena cae en fin de semana.
La regla es: el calendario es un dato que se consulta, no una condición escrita en el código. Puede venir de una librería mantenida, de una tabla en tu base de datos o del propio bróker, pero tiene que ser algo que se pueda actualizar sin desplegar y revisar sin leer código.
import pandas_market_calendars as mcal
nyse = mcal.get_calendar("XNYS")
# Semana de Accion de Gracias de 2026: el jueves 26 no aparece porque es festivo,
# y el viernes 27 aparece con cierre temprano.
sesiones = nyse.schedule(start_date="2026-11-23", end_date="2026-11-27")
print(sesiones.index) # dias con sesion, en fechas
print(sesiones["market_open"]) # apertura de cada dia, en hora universal
print(sesiones["market_close"]) # cierre de cada dia, en hora universalFíjate en un detalle que decide medio módulo: la librería devuelve instantes, no horas de pared. Eso significa que ya te ha resuelto el horario de verano y que puedes compararlos directamente con el reloj del servidor, que estará en hora universal como verás en el módulo cuatro.
Decidir si hoy se opera, y a qué hora exacta
La consulta no es "¿está abierto?", sino dos preguntas separadas: si hoy hay sesión y en qué instante exacto me toca actuar. Ese instante casi nunca es la apertura ni el cierre a secas, sino una hora derivada del horario real del día.
from datetime import timedelta
def sesion_de(cal, dia):
"""Devuelve (apertura, cierre) de ese dia, o None si no hay sesion."""
s = cal.schedule(start_date=dia, end_date=dia)
if s.empty:
return None
fila = s.iloc[0]
return fila["market_open"].to_pydatetime(), fila["market_close"].to_pydatetime()
def instante_de_ejecucion(cal, dia, minutos_antes=15):
sesion = sesion_de(cal, dia)
if sesion is None:
return None # festivo o fin de semana: no se opera
apertura, cierre = sesion
if cierre - apertura < timedelta(hours=4):
minutos_antes = 5 # media sesion: la ventana util se encoge
objetivo = cierre - timedelta(minutes=minutos_antes)
return max(objetivo, apertura + timedelta(minutes=5))El planificador puede seguir despertando cada pocos minutos: quien decide no es el reloj de cron, es el calendario. La tarea se despierta, pregunta, y en la inmensa mayoría de los despertares no hace nada.
def toca_actuar(cal, ahora_utc, tolerancia_seg=180):
objetivo = instante_de_ejecucion(cal, ahora_utc.date())
if objetivo is None:
return False
retraso = (ahora_utc - objetivo).total_seconds()
return 0 <= retraso <= tolerancia_seg # ni antes de tiempo ni con retraso viejoLa tolerancia no es un adorno. Sin ella, un despertar que llegue tres segundos tarde no coincide nunca con el instante objetivo y el sistema no opera jamás; con una tolerancia infinita, una tarea que arranque a medianoche por un reinicio creería que todavía le toca la sesión de ayer.
Y hay un caso que conviene dejar decidido antes de que ocurra: qué haces si el calendario no se puede consultar porque la librería falla o la tabla no responde. La respuesta segura es la misma de siempre: si no sabes si hay sesión, no operas.
Escenario trabajado
Escenario trabajado, con cifras de ejemplo. Un sistema envía sus órdenes al cierre a las 15:45 hora de Nueva York, quince minutos antes del cierre habitual, con la hora escrita a mano. El viernes siguiente a Acción de Gracias el mercado cierra a la una del mediodía: a las 15:45 llevaba casi tres horas cerrado. Ese día el sistema no operó, se quedó con la posición del día anterior durante todo el fin de semana largo y nadie lo detectó hasta el lunes.
El coste no fue el hueco de un día, sino que la cartera quedó desviada del objetivo justo antes de tres sesiones sin poder ajustar. Con el instante derivado del calendario, la ejecución habría salido a las 12:55 hora local sin tocar una línea. Formación, no asesoramiento financiero.