---
title: "0.4.4 Ficheros, pathlib y zonas horarias"
description: "Leer y escribir CSV, JSON, YAML y TOML con pathlib, y convertir horas entre mercados con zoneinfo: por qué Nueva York abre a las 14:30 algunas semanas."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/ficheros-pathlib-y-zonas-horarias/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/ficheros-pathlib-y-zonas-horarias.png"
---

# 0.4.4 · 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.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.4 · Python intermedio
- Duración: 50 min
- Requisitos: [0.4.3 · Iteradores, generadores, decoradores y context managers](https://pythonparatrading.com/curso-python-trading/nivel-0/iteradores-generadores-y-decoradores/)

## Al terminar esta lección

- Construir rutas con `pathlib` y leer y escribir CSV, JSON, YAML y TOML con la biblioteca estándar y PyYAML.
- Reconocer las trampas de cada formato: tipos que llegan como texto, fechas que JSON no admite y símbolos que YAML convierte en booleanos.
- Crear fechas con zona horaria con `zoneinfo`, convertirlas entre mercados y guardarlas en UTC.
- Calcular las semanas en que la diferencia horaria entre Nueva York y Madrid cambia, y explicar las horas que no existen o se repiten.

## 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:

```python
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())
```
```output
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 True
```

`glob("*.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**.

```python
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}")
```
```output
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.

```python
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"))
```
```output
simbolo,pnl
ORO,-3237.96
RVAR,17.09
SINT,4405.08
TECH,-35.02
```

Una 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.

```python
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)
```
```output
TypeError: Object of type date is not JSON serializable
2025-12-31 str
True
```

La 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:

```python
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")))
```
```output
{'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"`):

```python
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()))
```
```output
{'nombre': 'cruce', 'simbolos': ['SPY', 'ON', 'NO'], 'inicio': datetime.date(2021, 1, 4)}
4.5
```

Los 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.[^1] Convertir la apertura a otras zonas es
una llamada a `astimezone`:

```python
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"))
```
```output
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 UTC
```

La 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;[^2] la Unión Europea lo hace el **último** domingo
de marzo y el último de octubre.[^3] Entre esas fechas, la diferencia entre las dos ciudades es de 5 horas en lugar de
6. En 2026, en días hábiles:

```python
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")
```
```output
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ías
```

Veinte 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:

```python
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)
```
```output
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 datetimes
```

La 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.

<aside class="callout" data-kind="riesgo">

Una misma fecha no es un mismo instante. La sesión continua de la Bolsa española cierra a las 17:30 de Madrid;[^4] la
NYSE, a las 16:00 de Nueva York, que son las 22:00 de Madrid. Usar el cierre de un índice estadounidense del día D para
decidir una operación al cierre de un valor español del mismo día D es usar información que todavía no existía:
sesgo de anticipación (lección 0.1.1).

</aside>

El desfase entre cierres se calcula igual que antes, cada uno en su zona:

```python
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")
```
```output
BME   16:30 UTC
NYSE  21:00 UTC
NYSE cierra 4:30:00 después
```

Las 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.

[^1]: NYSE, «Holidays & Trading Hours»: *Core Trading Session: 9:30 a.m. to 4:00 p.m. ET*. Enlace en «Recursos».
[^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».
[^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».
[^4]: 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).

## Ejercicios

1. 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.**

```python
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)"))
```
```output
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.

2. Lee `operaciones_sint.csv` y escribe en `largas.json` solo las operaciones largas de SINT, con `cantidad` como
   entero, los precios como float y las fechas como texto ISO. Comprueba cuántas hay leyendo el JSON.

**Solución.**

```python
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])
```
```output
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.

3. Escribe una función `en_sesion(instante, zona, abre, cierra)` que diga si un `datetime` aware 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.**

```python
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)))
```
```output
2026-03-16 True
2026-03-30 False
```

A 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.

## Autoevaluación

1. Un fichero YAML contiene `simbolos: [SPY, ON, NO]`. ¿Qué devuelve `yaml.safe_load`?

   - A. `['SPY', 'ON', 'NO']`
   - B. `['SPY', True, False]`
   - C. Un error de sintaxis
   - D. `['SPY', None, None]`

   *Respuesta B: `['SPY', True, False]`.* PyYAML sigue YAML 1.1, donde `on`, `off`, `yes` y `no` (en cualquier combinación de mayúsculas) son booleanos. Los símbolos que coincidan con esas palabras hay que ponerlos entre comillas.

2. ¿A qué hora de Madrid abre la NYSE el lunes 16 de marzo de 2026?

   - A. 15:30, como siempre
   - B. 14:30, porque Estados Unidos ya ha adelantado la hora y Europa todavía no
   - C. 16:30
   - D. 09:30

   *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.

3. ¿Por qué conviene guardar las marcas de tiempo en UTC?

   - A. Porque ocupa menos espacio
   - B. Porque UTC no tiene cambios de hora: cada instante tiene una sola representación y las diferencias entre marcas son siempre correctas
   - C. Porque los mercados abren en UTC
   - D. Porque Python no admite otras zonas

   *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.

4. Comparas `datetime(2026, 3, 9, 15, 0)` (sin zona) con una apertura creada con `tzinfo=ZoneInfo("America/New_York")`. ¿Qué ocurre?

   - A. Python supone que la primera está en UTC
   - B. Python supone que la primera está en la hora local del ordenador
   - C. `TypeError`: no se pueden comparar fechas con y sin zona horaria
   - D. Devuelve siempre `False`

   *Respuesta C: `TypeError`: no se pueden comparar fechas con y sin zona horaria.* Una 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.Path` construye rutas con `/`, lista ficheros con `glob` y 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: `ON` y `NO` son booleanos y `1.10` es el número 1.1. TOML exige comillas en los textos y no tiene ese problema; `tomllib` lo 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.

## Recursos

### Documentación

- [Documentación de Python 3.14: zoneinfo](https://docs.python.org/3.14/library/zoneinfo.html) · docs.python.org
- [Documentación de Python 3.14: pathlib](https://docs.python.org/3.14/library/pathlib.html) · docs.python.org
- [NYSE: horarios y festivos de negociación](https://www.nyse.com/markets/hours-calendars) · nyse.com
- [Real Decreto 236/2002, por el que se establece la hora de verano (BOE)](https://www.boe.es/eli/es/rd/2002/03/01/236) · boe.es
- [NIST: reglas del horario de verano en Estados Unidos](https://www.nist.gov/pml/time-and-frequency-division/popular-links/daylight-saving-time-dst) · nist.gov

### Datos

- [Datos del nivel 0: operaciones del ETF ficticio SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/operaciones_sint.csv) · Descarga · operaciones_sint.csv

Lección anterior: [0.4.3 · Iteradores, generadores, decoradores y context managers](https://pythonparatrading.com/curso-python-trading/nivel-0/iteradores-generadores-y-decoradores/)

Lección siguiente: [0.4.5 · Logging profesional y testing con pytest](https://pythonparatrading.com/curso-python-trading/nivel-0/logging-y-testing-con-pytest/)
