Lección 0.4.4 · 50 min
Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
Los datos de un sistema de trading viven en ficheros y sus horas viven en zonas horarias. Esta lección cubre los cuatro formatos de texto habituales con pathlib y el módulo zoneinfo, con el que la apertura de Nueva York deja de ser «a las 15:30» y pasa a ser lo que es: una hora local que se mueve.
- Módulo
- 0.4 · Python intermedio
- Duración
- 50 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Rutas con pathlib
Un sistema de trading lee precios, configuraciones y operaciones, y escribe informes, registros y resultados de
backtests. Antes de abrir un fichero hay que saber dónde está, y las rutas escritas como texto
("datos/" + simbolo + ".csv") se rompen con facilidad: barras de más o de menos, separadores distintos en Windows y
Linux, extensiones duplicadas.
pathlib.Path representa una ruta como un objeto. El operador / une partes, y sus atributos dan el nombre, la
extensión o la carpeta sin manipular texto:
from pathlib import Path
for p in sorted(Path(".").glob("*.csv")):
print(f"{p.name:<22} {p.stem:<16} {p.stat().st_size:>8,} B")
informes = Path("informes") / "2025" / "sint"
informes.mkdir(parents=True, exist_ok=True)
destino = informes / "resumen.csv"
print(destino, destino.suffix, destino.parent.exists())cartera_sint.csv cartera_sint 49,207 B
operaciones_sint.csv operaciones_sint 2,440 B
sint_diario.csv sint_diario 60,757 B
informes/2025/sint/resumen.csv .csv Trueglob("*.csv") lista los ficheros que encajan con el patrón (rglob busca también en subcarpetas). mkdir con
parents=True crea las carpetas intermedias y con exist_ok=True no falla si ya existen: un script de informes puede
ejecutarse cada día sin comprobar nada antes.
Las rutas relativas, como Path(".") o "sint_diario.csv", se resuelven desde el directorio de trabajo, el
lugar desde el que se lanzó el programa (lección 0.2.4). Un script que funciona en la terminal y falla en un
programador de tareas casi siempre tiene ese problema. La solución habitual es construir las rutas a partir de la
ubicación del propio script: Path(__file__).parent / "datos".
CSV: todo llega como texto
CSV es el formato más común para precios y operaciones: una fila por registro, columnas separadas por comas y una
cabecera. El módulo csv de la biblioteca estándar se encarga de las comillas y los separadores; DictReader
entrega cada fila como un diccionario con las claves de la cabecera. Lo que no hace es convertir tipos: todo llega
como texto.
import csv
with open("operaciones_sint.csv", newline="", encoding="utf-8") as f:
filas = list(csv.DictReader(f))
print(len(filas), list(filas[0]))
print(filas[0]["cantidad"], type(filas[0]["cantidad"]).__name__)
def pnl(fila: dict[str, str]) -> float:
signo = 1 if fila["lado"] == "largo" else -1
bruto = signo * int(fila["cantidad"]) * (
float(fila["precio_salida"]) - float(fila["precio_entrada"]))
return bruto - float(fila["comision"])
por_simbolo: dict[str, float] = {}
for fila in filas:
s = fila["simbolo"]
por_simbolo[s] = por_simbolo.get(s, 0.0) + pnl(fila)
for s, total in sorted(por_simbolo.items()):
print(f"{s:<5} {total:>10,.2f}")40 ['id', 'fecha_entrada', 'fecha_salida', 'simbolo', 'lado', 'cantidad', 'precio_entrada', 'precio_salida', 'comision']
200 str
ORO -3,237.96
RVAR 17.09
SINT 4,405.08
TECH -35.02"200" es texto: sumarlo daría un error y multiplicarlo repetiría la cadena (lección 0.4.2). Por eso cada campo
numérico pasa por int() o float() antes de operar. pandas (lección 0.5.2) infiere los tipos solo, y aun así
conviene revisarlos.
Escribir es simétrico. csv.writer recibe filas como listas y se encarga de separadores y comillas. El argumento
newline="" en open no es decorativo: sin él, en Windows cada fila acabaría con un salto de línea de más.
with destino.open("w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["simbolo", "pnl"])
for s, total in sorted(por_simbolo.items()):
w.writerow([s, round(total, 2)])
print(destino.read_text(encoding="utf-8"))simbolo,pnl
ORO,-3237.96
RVAR,17.09
SINT,4405.08
TECH,-35.02Una trampa típica en España: los CSV exportados desde una hoja de cálculo en español usan ; como separador y coma
decimal (142,22). float("142,22") lanza ValueError. Se lee con csv.reader(f, delimiter=";") y se cambia la
coma por un punto antes de convertir, o con pd.read_csv(sep=";", decimal=","). Lo que nunca hay que hacer es
quitar la coma: 14222 es otro precio.
JSON: sin fechas ni decimales
JSON es el formato de las APIs: casi todos los brókeres y proveedores de datos responden en JSON. Admite objetos
(diccionarios), listas, texto, números, true, false y null. Nada más: ni fechas, ni Decimal, ni conjuntos.
import json
from datetime import date
resumen = {"estrategia": "cruce-50-200", "fecha": date(2025, 12, 31),
"operaciones": len(filas),
"pnl": {s: round(v, 2) for s, v in por_simbolo.items()}}
try:
json.dumps(resumen)
except TypeError as e:
print("TypeError:", e)
resumen["fecha"] = resumen["fecha"].isoformat()
texto = json.dumps(resumen, ensure_ascii=False, indent=2)
Path("resumen.json").write_text(texto, encoding="utf-8")
leido = json.loads(Path("resumen.json").read_text(encoding="utf-8"))
print(leido["fecha"], type(leido["fecha"]).__name__)
print(leido == resumen)TypeError: Object of type date is not JSON serializable
2025-12-31 str
TrueLa fecha se guarda como texto ISO 8601 (2025-12-31) y vuelve como texto: quien lee el JSON tiene que saber que ese
campo es una fecha y convertirlo con date.fromisoformat. Es el trabajo que hace Pydantic (lección 0.4.2) cuando
valida la respuesta de una API: el modelo dice que el campo es una fecha y Pydantic la convierte.
Dos detalles más. ensure_ascii=False deja las tildes como tales en lugar de ó. Y json.dumps escribe
NaN para un float no numérico, que no es JSON estándar: muchos lectores, incluidos los navegadores, lo rechazan.
Con allow_nan=False el error salta al escribir y no al leer.
YAML y TOML: configuraciones
Las configuraciones se escriben a mano, y para eso JSON es incómodo: no admite comentarios y exige comillas en todo.
Los dos formatos habituales son YAML y TOML. YAML se lee con PyYAML (lección 0.4.2); TOML, con tomllib, de la
biblioteca estándar desde Python 3.11.
YAML adivina los tipos a partir del texto, y a veces adivina mal. PyYAML sigue la versión 1.1 del estándar, donde
yes, no, on y off son booleanos. Hay empresas que cotizan con esos símbolos:
import yaml
Path("config.yaml").write_text("""\
simbolos: [SPY, ON, NO, "ON"]
inicio: 2021-01-04
version_modelo: 1.10
""", encoding="utf-8")
print(yaml.safe_load(Path("config.yaml").read_text(encoding="utf-8"))){'simbolos': ['SPY', True, False, 'ON'], 'inicio': datetime.date(2021, 1, 4), 'version_modelo': 1.1}ON (onsemi, en el Nasdaq) se ha convertido en True y NO en False; solo el "ON" entre comillas sigue siendo
texto. La fecha sin comillas se ha convertido en un date y la versión 1.10, en el número 1.1. En una lista de
símbolos el error no lanza ninguna excepción: la estrategia simplemente no opera esos activos, o falla mucho después
al pedir el precio de True. Por eso las configuraciones se validan con un modelo (lección 0.4.2) que exige
list[str].
TOML exige comillas en todos los textos, así que no tiene ese problema. Es el formato de pyproject.toml
(lección 0.2.2). tomllib.load necesita el fichero abierto en modo binario ("rb"):
import tomllib
Path("estrategia.toml").write_text("""\
[estrategia]
nombre = "cruce"
simbolos = ["SPY", "ON", "NO"]
inicio = 2021-01-04
[costes]
comision_pb = 1.5
deslizamiento_pb = 3.0
""", encoding="utf-8")
with open("estrategia.toml", "rb") as f:
cfg = tomllib.load(f)
print(cfg["estrategia"])
print(sum(cfg["costes"].values())){'nombre': 'cruce', 'simbolos': ['SPY', 'ON', 'NO'], 'inicio': datetime.date(2021, 1, 4)}
4.5Los símbolos siguen siendo texto y la fecha, una fecha. tomllib solo lee: para escribir TOML hace falta un paquete
externo (tomli-w). Encaja con su uso: una configuración se escribe a mano y el programa la lee.
| Formato | Lectura en Python | Comentarios | Tipos | Uso típico |
|---|---|---|---|---|
| CSV | csv (stdlib), pandas | No | Todo texto | Precios, operaciones |
| JSON | json (stdlib) | No | Básicos, sin fechas | APIs, resultados |
| YAML | PyYAML (safe_load) | Sí | Los adivina (cuidado) | Configuraciones |
| TOML | tomllib (stdlib, solo lee) | Sí | Explícitos, con fechas | pyproject.toml, configuraciones |
Fechas con zona horaria: zoneinfo
Un datetime sin zona horaria (naive) es una hora de reloj sin lugar: «9 de marzo de 2026, 09:30» no dice si es
en Nueva York, en Madrid o en Tokio. Uno con zona (aware) sí representa un instante concreto. zoneinfo, en la
biblioteca estándar desde Python 3.9, da las zonas de la base de datos IANA, con su historia completa de cambios de
hora, por nombre: America/New_York, Europe/Madrid, Asia/Tokyo.
La sesión principal de la NYSE va de 09:30 a 16:00, hora de Nueva York.1Nota 1 · NYSE, «Holidays & Trading Hours»: Core Trading Session: 9:30 a.m. to 4:00 p.m. ET. Enlace en «Recursos». Convertir la apertura a otras zonas es
una llamada a astimezone:
from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
NY = ZoneInfo("America/New_York")
MAD = ZoneInfo("Europe/Madrid")
UTC = ZoneInfo("UTC")
for dia in (date(2026, 3, 2), date(2026, 3, 9), date(2026, 3, 30)):
apertura = datetime.combine(dia, time(9, 30), NY)
print(apertura.isoformat(), "→",
apertura.astimezone(MAD).strftime("%H:%M Madrid"), "|",
apertura.astimezone(UTC).strftime("%H:%M UTC"))2026-03-02T09:30:00-05:00 → 15:30 Madrid | 14:30 UTC
2026-03-09T09:30:00-04:00 → 14:30 Madrid | 13:30 UTC
2026-03-30T09:30:00-04:00 → 15:30 Madrid | 13:30 UTCLa misma apertura, a la misma hora de Nueva York, cae a las 15:30 de Madrid el 2 de marzo, a las 14:30 el 9 y otra vez a las 15:30 el 30. El desfase sale de las reglas del cambio de hora: Estados Unidos adelanta el reloj el segundo domingo de marzo y lo retrasa el primero de noviembre;2Nota 2 · NIST, «Daylight Saving Time Rules»: el horario de verano empieza el segundo domingo de marzo a las 2:00 y termina el primer domingo de noviembre a las 2:00, hora local. Enlace en «Recursos». la Unión Europea lo hace el último domingo de marzo y el último de octubre.3Nota 3 · Directiva 2000/84/CE, incorporada en España por el Real Decreto 236/2002: el último domingo de marzo y el último domingo de octubre, a la 1:00 UTC. Enlace en «Recursos». Entre esas fechas, la diferencia entre las dos ciudades es de 5 horas en lugar de 6. En 2026, en días hábiles:
def diferencia(dia: date) -> timedelta:
"""Horas entre Madrid y Nueva York a la apertura de la NYSE."""
ap = datetime.combine(dia, time(9, 30), NY)
return ap.astimezone(MAD).utcoffset() - ap.utcoffset()
dias = [date(2026, 1, 1) + timedelta(days=i) for i in range(365)]
habiles = [d for d in dias if d.weekday() < 5]
raros = [d for d in habiles if diferencia(d) != timedelta(hours=6)]
print(len(raros), "días hábiles con", diferencia(raros[0]))
tramos = [[raros[0]]]
for ayer, hoy in zip(raros, raros[1:]):
if (hoy - ayer).days > 3:
tramos.append([])
tramos[-1].append(hoy)
for t in tramos:
print(f" del {t[0]} al {t[-1]}: {len(t)} días")20 días hábiles con 5:00:00
del 2026-03-09 al 2026-03-27: 15 días
del 2026-10-26 al 2026-10-30: 5 díasVeinte días hábiles de 2026 en los que Nueva York abre una hora antes para quien opera desde Madrid: del 9 al 27 de marzo y del 26 al 30 de octubre. La figura de la cabecera dibuja cada lunes de 2026 con la hora de Madrid de la apertura; las cuatro semanas destacadas son las del desfase.
Las consecuencias son prácticas. Un programa que lanza las órdenes «a las 15:25 de Madrid, cinco minutos antes de la apertura» llega una hora tarde esas semanas. Un filtro que descarta «la primera media hora de Nueva York» con horas de Madrid fijas se aplica a la hora equivocada. Y un backtest con barras intradía en hora local de Madrid mezcla dos horarios de sesión sin que nadie lo note. La solución es siempre la misma: expresar los horarios de cada mercado en su propia zona y convertir.
Horas que no existen y horas que se repiten
El cambio de hora crea dos anomalías en la hora local. En Madrid, el 29 de marzo de 2026 el reloj salta de 02:00 a
03:00: las 02:30 de ese día no existen. El 25 de octubre, de 03:00 vuelve a 02:00: las 02:30 ocurren dos
veces. zoneinfo no lanza errores; el atributo fold (0 o 1) elige entre las dos ocurrencias de una hora repetida:
inexistente = datetime(2026, 3, 29, 2, 30, tzinfo=MAD)
print(inexistente.astimezone(UTC).astimezone(MAD))
for fold in (0, 1):
repetida = datetime(2026, 10, 25, 2, 30, tzinfo=MAD, fold=fold)
print(fold, repetida.astimezone(UTC).strftime("%H:%M UTC"),
repetida.tzname())
try:
print(datetime(2026, 3, 9, 15, 0) < datetime.now(UTC))
except TypeError as e:
print("TypeError:", e)2026-03-29 03:30:00+02:00
0 00:30 UTC CEST
1 01:30 UTC CET
TypeError: can't compare offset-naive and offset-aware datetimesLa hora inexistente, ida y vuelta por UTC, se convierte en las 03:30 de verano. La repetida tiene dos instantes distintos, separados una hora: 00:30 y 01:30 UTC. Y Python se niega a comparar una fecha sin zona con otra con zona, en lugar de suponer nada.
Los mercados casi nunca negocian a esas horas, pero los datos sí se generan: los ficheros de criptomonedas, que cotizan las 24 horas, o los registros de un servidor en hora local, tienen una hora duplicada cada otoño y un hueco cada primavera. Las reglas que evitan casi todos los problemas:
- Guarda en UTC. Las marcas de tiempo de datos, órdenes y registros van en UTC, que no tiene cambios de hora.
- Convierte al mostrar. La hora local es para personas: informes, gráficos, avisos.
- Define los horarios de mercado en su zona. «09:30
America/New_York», no «15:30 de Madrid». - Nunca mezcles naive y aware. Si un fichero trae fechas sin zona, decide al leerlo en qué zona están y conviértelas.
El desfase entre cierres se calcula igual que antes, cada uno en su zona:
dia = date(2025, 12, 31)
cierre_bme = datetime.combine(dia, time(17, 30), MAD)
cierre_nyse = datetime.combine(dia, time(16, 0), NY)
print(cierre_bme.astimezone(UTC).strftime("BME %H:%M UTC"))
print(cierre_nyse.astimezone(UTC).strftime("NYSE %H:%M UTC"))
print("NYSE cierra", cierre_nyse - cierre_bme, "después")BME 16:30 UTC
NYSE 21:00 UTC
NYSE cierra 4:30:00 despuésLas restas entre fechas aware dan la duración real, porque Python las convierte a UTC antes de restar. Restar dos horas locales sin zona en días de cambio de hora daría una hora de más o de menos.
Ejercicios
- Calcula a qué hora de Madrid abre la Bolsa de Tokio (09:00,
Asia/Tokyo) el 2 de marzo y el 30 de marzo de 2026. ¿Por qué cambia, si Japón no tiene horario de verano?
Solución
TOK = ZoneInfo("Asia/Tokyo")
for dia in (date(2026, 3, 2), date(2026, 3, 30)):
ap = datetime.combine(dia, time(9, 0), TOK)
print(dia, ap.astimezone(MAD).strftime("%H:%M Madrid (%Z)"))2026-03-02 01:00 Madrid (CET)
2026-03-30 02:00 Madrid (CEST)Japón no cambia de hora; cambia Madrid. En invierno (CET, UTC+1) Tokio va 8 horas por delante; en verano (CEST, UTC+2), 7. Un horario de mercado se mueve visto desde fuera aunque el mercado no cambie nada.
- Lee
operaciones_sint.csvy escribe enlargas.jsonsolo las operaciones largas de SINT, concantidadcomo entero, los precios como float y las fechas como texto ISO. Comprueba cuántas hay leyendo el JSON.
Solución
largas = [
{"entrada": f["fecha_entrada"], "salida": f["fecha_salida"],
"cantidad": int(f["cantidad"]),
"precio_entrada": float(f["precio_entrada"]),
"precio_salida": float(f["precio_salida"])}
for f in filas if f["simbolo"] == "SINT" and f["lado"] == "largo"
]
Path("largas.json").write_text(json.dumps(largas, indent=2),
encoding="utf-8")
datos = json.loads(Path("largas.json").read_text(encoding="utf-8"))
print(len(datos), datos[0])24 {'entrada': '2021-09-02', 'salida': '2021-10-12', 'cantidad': 180, 'precio_entrada': 108.64, 'precio_salida': 108.79}Las fechas ya llegan del CSV como texto ISO, así que se copian tal cual. Las cantidades y los precios se convierten antes de escribir: el JSON guarda números, y quien lo lea no tendrá que convertirlos.
- Escribe una función
en_sesion(instante, zona, abre, cierra)que diga si undatetimeaware cae dentro del horario de un mercado expresado en su zona. Comprueba las 15:00 de Madrid del 16 y del 30 de marzo de 2026 contra la NYSE.
Solución
def en_sesion(instante: datetime, zona: ZoneInfo,
abre: time, cierra: time) -> bool:
local = instante.astimezone(zona)
return local.weekday() < 5 and abre <= local.time() < cierra
for dia in (16, 30):
t = datetime(2026, 3, dia, 15, 0, tzinfo=MAD)
print(t.date(), en_sesion(t, NY, time(9, 30), time(16, 0)))2026-03-16 True
2026-03-30 FalseA las 15:00 de Madrid del 16 de marzo la NYSE ya lleva media hora abierta (son las 10:00 en Nueva York); el 30 de marzo todavía no ha abierto (son las 09:00). La función no sabe nada de festivos: un calendario de mercado completo (nivel 1) añade la lista de días sin sesión.
Footnotes
-
NYSE, «Holidays & Trading Hours»: Core Trading Session: 9:30 a.m. to 4:00 p.m. ET. Enlace en «Recursos». ↩
-
NIST, «Daylight Saving Time Rules»: el horario de verano empieza el segundo domingo de marzo a las 2:00 y termina el primer domingo de noviembre a las 2:00, hora local. Enlace en «Recursos». ↩
-
Directiva 2000/84/CE, incorporada en España por el Real Decreto 236/2002: el último domingo de marzo y el último domingo de octubre, a la 1:00 UTC. Enlace en «Recursos». ↩
-
BME Exchange, «Horario Bolsa Española»: mercado abierto de 9:00 a 17:30 y subasta de cierre de 17:30 a 17:35 (https://www.bolsasymercados.es/es/bme-exchange/horario-del-mercado.html). ↩
Respuesta plegada en cada pregunta
Autoevaluación
Un fichero YAML contiene
simbolos: [SPY, ON, NO]. ¿Qué devuelveyaml.safe_load?- A
['SPY', 'ON', 'NO'] - B
['SPY', True, False] - CUn error de sintaxis
- D
['SPY', None, None]
Ver respuesta
Respuesta B.
['SPY', True, False]PyYAML sigue YAML 1.1, donde
on,off,yesyno(en cualquier combinación de mayúsculas) son booleanos. Los símbolos que coincidan con esas palabras hay que ponerlos entre comillas.- A
¿A qué hora de Madrid abre la NYSE el lunes 16 de marzo de 2026?
- A15:30, como siempre
- B14:30, porque Estados Unidos ya ha adelantado la hora y Europa todavía no
- C16:30
- D09:30
Ver respuesta
Respuesta B. 14:30, porque Estados Unidos ya ha adelantado la hora y Europa todavía no
Estados Unidos cambia de hora el segundo domingo de marzo (8 de marzo de 2026) y la UE el último (29 de marzo). Entre ambas fechas, la diferencia es de 5 horas en lugar de 6.
¿Por qué conviene guardar las marcas de tiempo en UTC?
- APorque ocupa menos espacio
- BPorque UTC no tiene cambios de hora: cada instante tiene una sola representación y las diferencias entre marcas son siempre correctas
- CPorque los mercados abren en UTC
- DPorque Python no admite otras zonas
Ver respuesta
Respuesta B. Porque UTC no tiene cambios de hora: cada instante tiene una sola representación y las diferencias entre marcas son siempre correctas
En hora local hay horas que no existen (el adelanto de primavera) y horas que se repiten (el retraso de otoño). En UTC no pasa: se guarda en UTC y se convierte a hora local solo al mostrar.
Comparas
datetime(2026, 3, 9, 15, 0)(sin zona) con una apertura creada contzinfo=ZoneInfo("America/New_York"). ¿Qué ocurre?- APython supone que la primera está en UTC
- BPython supone que la primera está en la hora local del ordenador
- C
TypeError: no se pueden comparar fechas con y sin zona horaria - DDevuelve siempre
False
Ver respuesta
Respuesta C.
TypeError: no se pueden comparar fechas con y sin zona horariaUna fecha sin zona (naive) no representa un instante concreto. Python se niega a compararla con una con zona (aware) en lugar de adivinar.
Ideas clave
pathlib.Pathconstruye rutas con/, lista ficheros congloby lee o escribe texto en una línea. Las rutas relativas dependen del directorio de trabajo.- CSV entrega texto: hay que convertir cada columna. JSON no admite fechas ni
Decimal: se serializan como texto ISO. - YAML es cómodo y traicionero:
ONyNOson booleanos y1.10es el número 1.1. TOML exige comillas en los textos y no tiene ese problema;tomlliblo lee pero no lo escribe. - Estados Unidos y la UE cambian de hora en fechas distintas: en 2026, la NYSE abre a las 14:30 de Madrid del 9 al 27 de marzo y del 26 al 30 de octubre.
- Guarda en UTC, convierte al mostrar y nunca mezcles fechas con y sin zona. El cierre de dos mercados con la misma fecha no ocurre a la misma hora.