---
title: "6.2.5 Sesgo de anticipación en LLMs"
description: "Un LLM recuerda lo que pasó antes de su fecha de corte: cómo detectarlo, estimar el corte efectivo y hacer un backtest honesto con modelos point-in-time."
url: "https://pythonparatrading.com/curso-python-trading/nivel-6/sesgo-de-anticipacion-en-llms/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-6/sesgo-de-anticipacion-en-llms.png"
---

# 6.2.5 · Sesgo de anticipación en LLMs: el modelo que ya conoce el futuro

> Un LLM entrenado con textos hasta 2024 «predice» muy bien lo que pasó en 2022: no porque sepa predecir, sino porque lo ha leído. Esta lección mide el efecto con cuatro modelos simulados y respuesta conocida, enseña a estimar el corte efectivo cuando el declarado no es fiable, calcula cuántas preguntas hacen falta para una evaluación honesta y comprueba con un LLM real que quitar el nombre de la empresa no basta.

- Curso: [Nivel 6: IA generativa, LLMs, agentes, big data y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-6/)
- Módulo: 6.2 · LLMs para investigación y trading
- Duración: 60 min
- Requisitos: [2.5.8 · Checklist de «backtest honesto» antes de pasar a paper trading](https://pythonparatrading.com/curso-python-trading/nivel-2/checklist-de-backtest-honesto/), [5.2.2 · Fundamentales point-in-time, macro y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-5/fundamentales-point-in-time-macro-y-datos-alternativos/)

## Al terminar esta lección

- Explicar por qué un backtest con un LLM sobre fechas anteriores a su corte de entrenamiento mide memoria y no capacidad de predecir.
- Medir el acierto y la rentabilidad de seguir a un modelo antes y después de su corte, y estimar el corte efectivo con una ruptura en el acierto.
- Diseñar una evaluación honesta: preguntas resueltas después del corte efectivo con margen, intervalo de confianza, referencia trivial y potencia suficiente.
- Conocer las defensas propuestas en la literatura (enmascarar entidades, modelos point-in-time, preguntas posteriores al corte) y sus límites.

## Un backtest que mide la memoria

El sesgo de anticipación (look-ahead bias) es viejo conocido del curso: usar en una fecha un dato que entonces no se
conocía (lecciones 2.5.8 y 5.2.2). Con un LLM aparece de una forma nueva y más difícil de ver. El modelo se entrenó con
billones de tokens de texto hasta una **fecha de corte**: noticias, informes, foros, cotizaciones comentadas. Si se le
pregunta en un backtest, con fecha de 2021, si una acción subirá el mes siguiente, y el modelo se entrenó con texto de
2023, la respuesta puede estar en su memoria. El backtest sale espectacular y no mide nada que se pueda repetir.

El problema es real y está documentado. Glasserman y Lin (2023) separan dos efectos al puntuar titulares con GPT: el
de anticipación (el modelo sabe cómo se movió el precio después) y el de «distracción» (lo que sabe de la empresa
interfiere con el tono del texto), y proponen anonimizar las empresas. Lopez-Lira, Tang y Zhu (2025) muestran que los
LLM recuerdan valores exactos de series económicas anteriores a su corte y ninguno posterior, y que ni pedirles que
respeten la fecha ni enmascarar nombres y fechas lo evita. Sarkar y Vafa (2025) proponen pruebas directas basadas en
sucesos imprevisibles con la información de la fecha y encuentran el sesgo en llamadas de resultados.

Como con un modelo real no se sabe qué ha leído, esta lección usa el banco de preguntas sintético del nivel 6, con
respuesta conocida y cuatro modelos **simulados** (no hay ningún LLM detrás; sus respuestas siguen reglas publicadas en
`sesgo6_modelos_verdad.csv`), y al final comprueba el mecanismo con el LLM local del curso.

```mermaid
flowchart LR
  A[Corpus hasta corte] --> B[Modelo]
  C[Pregunta fecha t] --> B
  B --> D{¿Antes del corte?}
  D -->|sí| E[Recuerda]
  D -->|no| F[Predice]
```

## Los datos: 620 preguntas y cuatro modelos

Cada pregunta tiene una fecha (cuando se haría en el backtest), una fecha de resolución (el primer día en que se conoce
la respuesta) y la respuesta correcta, calculada con los precios y las ventas sintéticos del nivel. Hay dos tipos: si
una acción cerrará por encima de su precio dentro de 21 sesiones y si las ventas del trimestre superarán al consenso.
Tres modelos declaran una fecha de corte; `modelo-pit` es **point-in-time**: en cada pregunta, su corte es la víspera.

```python
import numpy as np
import pandas as pd

q = pd.read_csv("sesgo6_preguntas.csv",
                parse_dates=["fecha_pregunta", "fecha_resolucion"])
resp = pd.read_csv("sesgo6_respuestas.csv")
modelos = pd.read_csv("sesgo6_modelos.csv")
print(modelos.to_string(index=False))
print(q["tipo"].value_counts().to_string())
ej = q.iloc[0]
print(ej["fecha_pregunta"].date(), "·", ej["pregunta"])
print("se resuelve el", ej["fecha_resolucion"].date(), "· respuesta:",
      ej["respuesta_correcta"])
```
```output
    modelo        fecha_corte_declarada
  modelo-a                   2021-09-30
  modelo-b                   2023-04-30
  modelo-c                   2024-10-31
modelo-pit día anterior a cada pregunta
tipo
precio_21_sesiones          420
ventas_frente_a_consenso    200
2019-03-01 · A cierre de 2019-03-01, Sofsa Software (SOFE) cotiza a 793,88 dólares. ¿Cerrará por encima de ese precio dentro de 21 sesiones?
se resuelve el 2019-04-01 · respuesta: no
```

## El backtest ingenuo

La estrategia más simple: comprar la acción si el modelo dice «sí» y venderla si dice «no», durante 21 sesiones. Las
preguntas usan el ticker vigente en su fecha y los precios, el actual: 11 preguntas son de empresas que cambiaron de
ticker después, el mismo problema de mapeo de entidades de la lección 6.7.5.

```python
precios = pd.read_parquet("acciones6_precios.parquet")
cierres = precios.pivot(index="fecha", columns="ticker",
                        values="cierre")
# las preguntas usan el ticker de su fecha; los precios, el actual
hist = pd.read_csv("acciones6_tickers.csv")
emp = pd.read_csv("acciones6_empresas.csv")
actual = (hist.merge(emp[["id_empresa", "ticker"]], on="id_empresa",
                     suffixes=("", "_hoy"))
          .set_index("ticker")["ticker_hoy"])
print("preguntas con ticker antiguo:",
      int((q["ticker"] != q["ticker"].map(actual)).sum()))
i = cierres.index.get_indexer(q["fecha_pregunta"])
cols = cierres.columns.get_indexer(q["ticker"].map(actual))
valores = cierres.to_numpy()
q["r21"] = valores[i + 21, cols] / valores[i, cols] - 1

d = resp.merge(q, on="id_pregunta")
d["acierto"] = d["respuesta"] == d["respuesta_correcta"]
d["pnl"] = np.where(d["respuesta"] == "sí", 1, -1) * d["r21"]
precio = d["tipo"] == "precio_21_sesiones"
resumen = pd.DataFrame({
    "acierto (%)": 100 * d.groupby("modelo")["acierto"].mean(),
    "rend. 21 ses. (%)":
        100 * d[precio].groupby("modelo")["pnl"].mean()})
print(resumen.round(1).to_string())
largo = q.loc[q["tipo"] == "precio_21_sesiones", "r21"].mean()
print(f"siempre comprado: {100 * largo:.1f} %")
```
```output
preguntas con ticker antiguo: 11
            acierto (%)  rend. 21 ses. (%)
modelo
modelo-a           65.6                2.5
modelo-b           79.8                4.6
modelo-c           87.4                5.9
modelo-pit         50.8                0.7
siempre comprado: 1.1 %
```

En toda la muestra, el modelo más reciente parece el mejor: `modelo-c` acierta el 87,4 % y ganaría un 5,9 % medio en
cada operación de 21 sesiones, frente al 1,1 % de estar siempre comprado. `modelo-a`, el más antiguo, se queda en el
65,6 %, y el point-in-time en un 50,8 %, lo de una moneda. Un investigador que comparase modelos así elegiría el más
reciente y creería tener una estrategia excepcional. Lo que ha medido es cuánta parte de la muestra cae antes del corte
de cada modelo.

## Antes y después del corte

```python
corte = pd.to_datetime(modelos.set_index("modelo")
                       ["fecha_corte_declarada"], errors="coerce")
d["corte"] = d["modelo"].map(corte)
d["zona"] = np.where(d["fecha_resolucion"] <= d["corte"],
                     "antes", "después")

def t_media(x):
    return x.mean() / x.std(ddof=1) * np.sqrt(len(x))

filas = []
for (m, z), g in d.groupby(["modelo", "zona"]):
    p = g[g["tipo"] == "precio_21_sesiones"]["pnl"]
    filas.append({"modelo": m, "zona": z, "n": len(g),
                  "acierto (%)": 100 * g["acierto"].mean(),
                  "rend. (%)": 100 * p.mean(), "t": t_media(p)})
print(pd.DataFrame(filas).round(2).to_string(index=False))
```
```output
    modelo    zona   n  acierto (%)  rend. (%)     t
  modelo-a   antes 237        89.45       6.15 10.50
  modelo-a después 383        50.91       0.24  0.31
  modelo-b   antes 389        93.06       6.31 12.34
  modelo-b después 231        57.58       1.66  1.56
  modelo-c   antes 517        90.91       6.46 11.70
  modelo-c después 103        69.90       3.35  4.04
modelo-pit después 620        50.81       0.69  1.22
```

Los rendimientos son los que documenta el README de los datos, y la tabla es la clave de la lección. Antes de su corte declarado, los tres modelos
aciertan el 89-93 % y seguirlos da un 6,2-6,5 % medio en 21 sesiones con $t > 10$: el rendimiento de saber el futuro.
Después, `modelo-a` cae al 50,9 % y a un 0,24 % por operación ($t = 0{,}31$). `modelo-b` y `modelo-c` quedan algo por
encima (57,6 % y 69,9 %), y la explicación está en la sección siguiente. El modelo point-in-time acierta el 50,8 % y gana
un 0,69 % ($t = 1{,}22$), menos que estar siempre comprado.

*Fig. — Rendimiento medio de comprar o vender a 21 sesiones según la respuesta de cada modelo, antes y después de su corte declarado (420 preguntas de precio).* Barras horizontales. Antes de su corte, seguir a los modelos A, B y C da un 6,15 %, 6,31 % y 6,46 % por operación (A destacado). Después, un 0,24 %, 1,66 % y 3,35 %. El modelo point-in-time da un 0,69 % y estar siempre comprado, un 1,15 %.

## El corte declarado no es el efectivo

La fecha de corte que publica un proveedor es una afirmación sobre su corpus, y un corpus puede incluir textos
posteriores: páginas actualizadas, datos de ajuste fino o de alineación, documentos con fecha errónea. En los datos,
`modelo-b` y `modelo-c` se entrenaron con algo de texto posterior al corte que declaran. La figura de la cabecera dibuja
el acierto por semestre de resolución:

```python
d["semestre"] = (d["fecha_resolucion"].dt.year.astype(str)
                 + np.where(d["fecha_resolucion"].dt.month <= 6,
                            "-1", "-2"))
sem = d.pivot_table(index="semestre", columns="modelo",
                    values="acierto", aggfunc="mean")
print((100 * sem).round(0).astype(int).to_string())
```
```output
modelo    modelo-a  modelo-b  modelo-c  modelo-pit
semestre
2019-1          81        90        90          43
2019-2          88        86        86          47
2020-1          89        93        89          50
2020-2          90        93        91          47
2021-1          91        98        93          60
2021-2          76        93        90          59
2022-1          43        96        94          51
2022-2          59        94        94          56
2023-1          50        89        84          48
2023-2          49        64        91          47
2024-1          56        61        94          58
2024-2          56        54        92          50
2025-1          47        51        62          51
2025-2          49        51        69          44
```

El acierto de `modelo-b` sigue en el 89 % en el primer semestre de 2023, cuando su corte declarado es el 30 de abril, y
solo cae al 64 % en el segundo. Si el acierto es alto mientras el modelo recuerda y bajo cuando predice, la fecha que
mejor separa las dos tasas es una estimación del corte efectivo. La celda la busca por máxima verosimilitud entre todos
los fines de mes, con dos tasas de acierto constantes (una ruptura de un modelo binomial):

```python
def ruptura(g):
    """Fin de mes que mejor separa dos tasas de acierto."""
    mejor = None
    for c in pd.date_range("2019-06-30", "2025-09-30", freq="ME"):
        a = g.loc[g["fecha_resolucion"] <= c, "acierto"]
        b = g.loc[g["fecha_resolucion"] > c, "acierto"]
        if min(len(a), len(b)) < 20:
            continue
        ll = 0.0
        for s in (a, b):
            p = np.clip(s.mean(), 1e-9, 1 - 1e-9)
            ll += s.sum() * np.log(p) + (~s).sum() * np.log(1 - p)
        if mejor is None or ll > mejor[1]:
            mejor = (c.date(), ll, a.mean(), b.mean())
    return mejor

for m in ("modelo-a", "modelo-b", "modelo-c"):
    c, _, pa, pb = ruptura(d[d["modelo"] == m])
    print(f"{m}: declarado {corte[m].date()} · estimado {c} "
          f"· acierto {pa:.0%} antes y {pb:.0%} después")
```
```output
modelo-a: declarado 2021-09-30 · estimado 2021-09-30 · acierto 89% antes y 51% después
modelo-b: declarado 2023-04-30 · estimado 2023-07-31 · acierto 93% antes y 55% después
modelo-c: declarado 2024-10-31 · estimado 2024-11-30 · acierto 91% antes y 66% después
```

Para `modelo-a` la estimación coincide con el corte declarado. Para `modelo-b` sale el 2023-07-31, tres meses después del
declarado, y para `modelo-c` el 2024-11-30, uno después. La verdad del generador (`sesgo6_modelos_verdad.csv`) da
cortes efectivos del 2023-09-30 y del 2024-12-31: la ruptura acierta la dirección (el corte real es posterior al
declarado) pero no la fecha exacta, porque en la zona gris el modelo acierta un 75 %, un valor intermedio, y los pocos
datos de cada mes no permiten precisar más. Conclusión práctica: el corte efectivo se estima, se toma con margen y se
comprueba con varias sondas.

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

Con un modelo real, las sondas habituales son preguntas cuya respuesta solo está en textos de una fecha conocida:
valores exactos de series publicadas (Lopez-Lira, Tang y Zhu, 2025), sucesos imprevisibles con la información previa
(Sarkar y Vafa, 2025) o la perplejidad del modelo sobre textos fechados (DatedGPT, 2026). Ninguna es definitiva; varias
juntas acotan el corte.

</aside>

## La confianza delata la memoria (en estos datos)

Los modelos simulados declaran una confianza con cada respuesta. En los datos, recordar se nota:

```python
alta = d["confianza"] >= 0.85
tabla = (d[d["modelo"] != "modelo-pit"]
         .assign(alta=alta)
         .groupby(["modelo", "zona"])
         .agg(confianza=("confianza", "mean"),
              alta=("alta", "mean")))
print((tabla * [1, 100]).round(2).to_string())
gris = d[(d["zona"] == "después") & alta & (d["modelo"] == "modelo-b")]
print("modelo-b, confianza alta tras el corte declarado:", len(gris),
      "respuestas, resueltas entre",
      gris["fecha_resolucion"].min().date(),
      "y", gris["fecha_resolucion"].max().date(),
      f"· acierto {gris['acierto'].mean():.0%}")
```
```output
                  confianza    alta
modelo   zona
modelo-a antes         0.92  100.00
         después       0.63    0.00
modelo-b antes         0.92  100.00
         después       0.66    5.19
modelo-c antes         0.92  100.00
         después       0.66    6.80
modelo-b, confianza alta tras el corte declarado: 12 respuestas, resueltas entre 2023-06-06 y 2023-09-27 · acierto 58%
```

Antes del corte, todas las respuestas tienen confianza de 0,85 o más (media 0,92); después, la media baja a 0,63-0,66 y
solo un 5-7 % supera 0,85. Las 12 respuestas de `modelo-b` con confianza alta después de su corte declarado se
resuelven entre junio y septiembre de 2023: la zona gris, dentro de su corte efectivo. Aquí la confianza es una huella
limpia de la memoria porque el generador la construye así. Con LLM reales, la confianza verbalizada o las
probabilidades de los tokens no separan tan bien lo recordado de lo inferido, y no hay que fiarse de ellas como única
sonda.

## Evaluación honesta

Una evaluación honesta de un modelo con fecha de corte necesita cuatro cosas: preguntas resueltas después del corte
**efectivo** con margen, una referencia trivial (aquí, responder siempre «sí», que acierta el 53,7 % porque las acciones
suben más veces de las que bajan), un intervalo de confianza y suficientes preguntas.

```python
from scipy.stats import binomtest

verdad = pd.read_csv("sesgo6_modelos_verdad.csv")
efectivo = pd.to_datetime(verdad.set_index("modelo")
                          ["fecha_corte_efectiva"], errors="coerce")
base_si = (q["respuesta_correcta"] == "sí").mean()

def honesta(g, corte_efectivo, margen_dias=30):
    g = g[g["fecha_resolucion"]
          > corte_efectivo + pd.Timedelta(days=margen_dias)]
    k, n = int(g["acierto"].sum()), len(g)
    ic = binomtest(k, n).proportion_ci(0.95)
    p = binomtest(k, n, base_si, alternative="greater").pvalue
    return n, k / n, ic.low, ic.high, p

print(f"referencia: responder siempre «sí» acierta {base_si:.1%}")
for m in ("modelo-a", "modelo-b", "modelo-c"):
    n, acc, lo, hi, p = honesta(d[d["modelo"] == m], efectivo[m])
    print(f"{m}: n = {n:3d} · acierto {acc:.1%} "
          f"(IC 95 %: {lo:.1%}-{hi:.1%}) · p frente a «sí» = {p:.2f}")
n, acc, lo, hi, p = honesta(d[d["modelo"] == "modelo-pit"],
                            pd.Timestamp("2018-12-31"))
print(f"modelo-pit: n = {n:3d} · acierto {acc:.1%} "
      f"(IC 95 %: {lo:.1%}-{hi:.1%}) · p frente a «sí» = {p:.2f}")
```
```output
referencia: responder siempre «sí» acierta 53.7%
modelo-a: n = 380 · acierto 50.8% (IC 95 %: 45.6%-55.9%) · p frente a «sí» = 0.88
modelo-b: n = 185 · acierto 55.1% (IC 95 %: 47.7%-62.4%) · p frente a «sí» = 0.38
modelo-c: n =  78 · acierto 66.7% (IC 95 %: 55.1%-76.9%) · p frente a «sí» = 0.01
modelo-pit: n = 620 · acierto 50.8% (IC 95 %: 46.8%-54.8%) · p frente a «sí» = 0.93
```

`modelo-a` y `modelo-b` no se distinguen de responder siempre «sí» (p = 0,88 y 0,38) y el point-in-time tampoco
(p = 0,93). `modelo-c` acierta el 66,7 % con p = 0,01. ¿Habilidad? La verdad del generador dice que fuera de su memoria
los tres aciertan un 52-55 %. Es una racha favorable en una muestra pequeña: 78 preguntas dan un intervalo del 55 % al
77 %, y al evaluar tres modelos, que uno salga significativo por azar no es raro (el problema de las pruebas múltiples de
la lección 2.5.4). Con datos reales no habría verdad publicada que lo desmintiera: solo esperar a que se resuelvan más
preguntas.

¿Cuántas harían falta? Con la aproximación normal de una prueba unilateral al 5 % y potencia del 80 %:

$$
n = \left( \frac{z_{1-\alpha} \sqrt{p_0 (1 - p_0)} + z_{1-\beta} \sqrt{p_1 (1 - p_1)}}{p_1 - p_0} \right)^2
$$

```python
from scipy.stats import norm

def n_necesario(p1, p0=0.5, alfa=0.05, potencia=0.8):
    za, zb = norm.ppf(1 - alfa), norm.ppf(potencia)
    num = za * np.sqrt(p0 * (1 - p0)) + zb * np.sqrt(p1 * (1 - p1))
    return int(np.ceil((num / (p1 - p0)) ** 2))

for p1 in (0.52, 0.55, 0.60):
    print(f"acierto real {p1:.0%} frente a 50 %: "
          f"{n_necesario(p1):5d} preguntas")
```
```output
acierto real 52% frente a 50 %:  3863 preguntas
acierto real 55% frente a 50 %:   617 preguntas
acierto real 60% frente a 50 %:   153 preguntas
```

Distinguir un 55 % de un 50 % exige 617 preguntas resueltas **después** del corte efectivo; un 52 %, casi 4 000. Un
modelo con corte reciente tiene pocos meses de historia honesta, así que la mayoría de las evaluaciones de LLM en
predicción de mercados no tienen potencia para detectar una ventaja realista. Es la razón práctica para preferir
modelos point-in-time: permiten usar toda la historia.

```mermaid
flowchart TB
  A[Preguntas fechadas] --> B[Corte efectivo]
  B --> C[Corte + margen]
  C --> D[Enmascarar]
  D --> E[Respuestas]
  E --> F[Referencia e IC]
  F --> G{¿Potencia?}
```

## Point-in-time: un modelo por fecha

La solución de raíz es que el modelo no haya leído nada posterior a la fecha de cada pregunta. Es lo que hace
`modelo-pit` en los datos, y lo que proponen varios trabajos recientes con modelos reales:

- **Time Machine GPT** (Drinkall et al., 2024): una serie de modelos entrenados solo con texto anterior a cada fecha,
  pensados para no tener conocimiento del futuro, publicados con sus datos de entrenamiento.
- **ChronoBERT y ChronoGPT** (He, Lv, Manela y Wu, 2025): modelos entrenados solo con el texto disponible en cada
  momento, competitivos en las pruebas de lenguaje con BERT; en una prueba de predicción de rentabilidades con noticias,
  sus ratios de Sharpe en tiempo real son comparables a los de un Llama mucho mayor, lo que sugiere que en esa aplicación
  el sesgo es moderado.
- **DatedGPT** (Yan et al., 2026): doce modelos de 1,3B parámetros con cortes anuales estrictos de 2013 a 2024; en su
  prueba con titulares, los modelos cuyo entrenamiento cubre el periodo de resultados muestran una «prima de
  anticipación» significativa frente a los que no.

Son modelos pequeños comparados con los comerciales. El intercambio es claro: un modelo point-in-time pierde capacidad y
gana una historia completa de backtest honesto. Lopez-Lira y Tang (2023), que estudiaron si ChatGPT predice
rentabilidades con titulares, usaron la otra vía: titulares posteriores al corte de conocimiento del modelo.

## Enmascarar entidades: necesario, no suficiente

Glasserman y Lin (2023) sustituyen el nombre de la empresa por un marcador para que el modelo no use lo que sabe de
ella. Con las preguntas del banco:

```python
import re

def enmascarar(pregunta, nombre, ticker):
    t = pregunta.replace(nombre, "la empresa A")
    t = t.replace(f" ({ticker})", "").replace(ticker, "A")
    t = re.sub(r"\b(19|20)\d{2}-\d{2}-\d{2}\b", "la fecha t", t)
    t = re.sub(r"\bde (19|20)\d{2}\b", "del año t", t)
    # el nivel de precio también identifica: se reescala a 100
    return re.sub(r"cotiza a [\d.,]+ dólares",
                  "cotiza a 100 (precio reescalado)", t)

nombres = emp.set_index("ticker")["nombre_corto"]
for _, f in q.iloc[[0, 1]].iterrows():
    nombre = nombres[actual[f["ticker"]]]
    print(enmascarar(f["pregunta"], nombre, f["ticker"]))
```
```output
A cierre de la fecha t, la empresa A cotiza a 100 (precio reescalado). ¿Cerrará por encima de ese precio dentro de 21 sesiones?
El consenso espera unas ventas de 4.563,2 millones de dólares de la empresa A en el cuarto trimestre del año t. ¿Las superará?
```

La función quita el nombre, el ticker y las fechas, y reescala el precio, porque un nivel de precio concreto (793,88
dólares) también identifica a una empresa y un día. Pero en la segunda pregunta queda el consenso de ventas, 4.563,2
millones de dólares: una cifra así, junto con el sector y el trimestre, puede bastar para reconocer a una empresa real.
Cada dato del prompt es una pista.

Lo mismo con un LLM de verdad. El modelo local del curso (Qwen2.5-0.5B-Instruct, publicado el 19 de septiembre de 2024
y entrenado con hasta 18 billones de tokens según su blog[^1]) es pequeño, pero recuerda hechos famosos. La quiebra de
Lehman Brothers fue el 15 de septiembre de 2008[^2]:

```python
import torch
import transformers
from transformers import AutoModelForCausalLM, AutoTokenizer

transformers.logging.set_verbosity_error()
torch.manual_seed(0)
torch.use_deterministic_algorithms(True)
NOMBRE = "Qwen/Qwen2.5-0.5B-Instruct"
tok = AutoTokenizer.from_pretrained(NOMBRE)
llm = AutoModelForCausalLM.from_pretrained(NOMBRE, dtype=torch.float32)

def pregunta(texto, n=12):
    x = tok.apply_chat_template([{"role": "user", "content": texto}],
                                add_generation_prompt=True,
                                return_tensors="pt", return_dict=True)
    with torch.inference_mode():
        o = llm.generate(**x, do_sample=False, max_new_tokens=n,
                         pad_token_id=tok.eos_token_id)
    return tok.decode(o[0, x["input_ids"].shape[1]:],
                      skip_special_tokens=True)

PROMPTS = {
    "con nombre": "¿En qué año quebró Lehman Brothers? "
                  "Responde solo con el año.",
    "sin nombre, con contexto": "Un gran banco de inversión de Nueva "
        "York se declaró en quiebra un 15 de septiembre, en plena "
        "crisis de las hipotecas subprime. ¿En qué año? "
        "Responde solo con el año.",
    "sin nombre ni contexto": "Un gran banco de inversión de Nueva "
        "York se declaró en quiebra un 15 de septiembre. ¿En qué año?"
        " Responde solo con el año.",
    "corte, en español": "¿Cuál es tu fecha de corte de conocimiento?"
                         " Responde solo con el mes y el año.",
    "corte, en inglés": "What is your knowledge cutoff date? "
                        "Answer with month and year only.",
}
for k, p in PROMPTS.items():
    print(f"{k:25s} -> {pregunta(p)!r}")
```
```output
con nombre                -> '2008'
sin nombre, con contexto  -> '2008'
sin nombre ni contexto    -> '2023'
corte, en español         -> 'Marzo 2019'
corte, en inglés          -> 'October 2023'
```

Con el nombre, responde «2008». Sin el nombre pero con el contexto de la crisis de las hipotecas, también «2008»: el
contexto identifica el hecho. Sin ningún contexto, «2023», y falla. Y la fecha de corte que dice el propio modelo no es
una fuente: «Marzo 2019» si se le pregunta en español y «October 2023» en inglés. Un modelo de 0,5B parámetros recuerda
poco; los modelos comerciales, con cientos de veces más parámetros, recuerdan mucho más, y por eso Lopez-Lira, Tang y Zhu
(2025) encuentran que enmascarar nombres y fechas no impide que reconstruyan el dato.

## Reglas para un backtest con LLM

- Fijar la **versión exacta** del modelo (no un alias que cambia) y registrar su corte declarado y la fuente de esa
  fecha.
- Estimar el **corte efectivo** con sondas y dejar margen; tratar el declarado como una cota inferior.
- Evaluar solo con preguntas **resueltas después** del corte efectivo, o usar un modelo point-in-time.
- **Enmascarar** nombres, tickers, fechas y cifras identificativas, sabiendo que no basta.
- Comparar con una **referencia trivial** (siempre «sí», siempre comprado, un modelo sin texto) con intervalo de
  confianza y una prueba pareada, y calcular la **potencia** antes de sacar conclusiones.
- Contar los modelos y prompts probados: elegir el mejor de varios en la misma muestra es selección (2.5.4).
- El ajuste fino propio también tiene fecha: un modelo ajustado con noticias de 2025 (lección 6.2.4) no se puede
  backtestear en 2022.

## Ejercicios

1. Repite la evaluación honesta de `modelo-b` usando el corte **declarado** en vez del efectivo, con margen de 0 días.
   ¿Cuánto se infla el acierto?

**Solución.**

```python
b = d[d["modelo"] == "modelo-b"]
for nombre, c, m in [("declarado, sin margen", corte["modelo-b"], 0),
                     ("efectivo + 30 días", efectivo["modelo-b"], 30)]:
    n, acc, lo, hi, p = honesta(b, c, margen_dias=m)
    print(f"{nombre:22s} n = {n:3d} · acierto {acc:.1%} "
          f"(IC {lo:.1%}-{hi:.1%}) · p = {p:.3f}")
```
```output
declarado, sin margen  n = 231 · acierto 57.6% (IC 50.9%-64.0%) · p = 0.133
efectivo + 30 días     n = 185 · acierto 55.1% (IC 47.7%-62.4%) · p = 0.377
```

Con el corte declarado entran en la evaluación las preguntas de la zona gris, que el modelo recuerda en parte. El
acierto sube y el p-valor baja: la evaluación parece mostrar una habilidad que es memoria de los meses en los que el
corpus ya cubría la respuesta.

2. Amplía `enmascarar` para que el consenso de ventas también se reescale (a 100) y aplícala a las 200 preguntas de
   ventas. ¿Queda alguna cifra que pueda identificar a la empresa?

**Solución.**

```python
def enmascarar2(pregunta, nombre, ticker):
    t = enmascarar(pregunta, nombre, ticker)
    return re.sub(r"[\d.]+,\d millones de dólares",
                  "100 (consenso reescalado)", t)

ventas = q[q["tipo"] == "ventas_frente_a_consenso"]
textos = [enmascarar2(f["pregunta"], nombres[actual[f["ticker"]]],
                      f["ticker"]) for _, f in ventas.iterrows()]
print(textos[0])
resto = {n for t in textos for n in re.findall(r"\d[\d.,]*", t)}
print("cifras que quedan en las 200:", sorted(resto))
```
```output
El consenso espera unas ventas de 100 (consenso reescalado) de la empresa A en el cuarto trimestre del año t. ¿Las superará?
cifras que quedan en las 200: ['100']
```

El año se ha convertido en «del año t», el importe en 100 y la empresa en «la empresa A»: la única cifra que queda en
las 200 preguntas es el 100 del reescalado. Aun así, el texto conserva información: el trimestre («cuarto») y la propia
pregunta. Con datos reales habría que comprobar también que el sector, el tamaño o una frase característica no
identifican al emisor, y medir el efecto del enmascarado comparando las respuestas con y sin él, como hacen Glasserman y
Lin (2023).

3. Un proveedor ofrece un LLM financiero con corte declarado en junio de 2024 y un backtest de 2015 a 2025 con Sharpe 3.
   ¿Qué le pedirías antes de creerlo?

**Solución.**

La parte del backtest posterior al corte efectivo, por separado: de 2015 a mediados de 2024 el modelo pudo leer los
resultados. Pediría cómo se ha estimado ese corte (y no solo el declarado), la versión exacta del modelo usada en cada
fecha y si ha cambiado, el número de modelos, prompts y variantes probados antes de elegir el que se presenta, y el
resultado de una referencia trivial en el mismo periodo con su intervalo de confianza. Con un año y pico de historia
honesta, un Sharpe alto tiene un error típico enorme (lección 2.5.4): lo razonable es esperar a acumular resultados
fuera de muestra o pedir una versión point-in-time del modelo.

[^1]: Blog de Qwen, «Qwen2.5: A Party of Foundation Models!», 19 de septiembre de 2024
    (https://qwenlm.github.io/blog/qwen2.5/, consultado el 2026-10-23). No publica una fecha de corte de conocimiento.

[^2]: Declaración de Thomas C. Baxter Jr. (Reserva Federal de Nueva York) ante la Financial Crisis Inquiry Commission, 1 de
    septiembre de 2010 (https://www.newyorkfed.org/newsevents/speeches/2010/bax100901): el 15 de septiembre, el consejo
    de Lehman llevó a la matriz al Chapter 11.

## Autoevaluación

1. Un LLM con corte declarado en septiembre de 2021 acierta el 89 % de 237 preguntas sobre si una acción subirá en el mes siguiente, todas resueltas antes de esa fecha, y el 51 % de las 383 resueltas después. ¿Qué mide el 89 %?

   - A. Lo que el modelo recuerda de su corpus de entrenamiento, no su capacidad de predecir
   - B. La capacidad del modelo en mercados tranquilos
   - C. Que el modelo se ha degradado con el tiempo
   - D. Un buen modelo que pierde capacidad por un cambio de régimen en 2022

   *Respuesta A: Lo que el modelo recuerda de su corpus de entrenamiento, no su capacidad de predecir.* Las respuestas ya estaban en el texto con el que se entrenó. Después del corte el acierto cae al de una moneda (51 %), que es lo que sabe hacer el modelo sin memoria. Un cambio de régimen no explica una caída de 38 puntos justo en la fecha de corte.

2. `modelo-b` declara un corte el 2023-04-30, pero su acierto sigue alto hasta el verano y la ruptura estimada cae el 2023-07-31. ¿Cómo debe tratarse la fecha declarada?

   - A. Como exacta, porque la publica el proveedor
   - B. Como una cota inferior: el corpus puede incluir algo de texto posterior, así que la evaluación honesta empieza después del corte efectivo estimado y con margen
   - C. Como una cota superior: el modelo sabe menos de lo que dice
   - D. Da igual, porque la diferencia es de tres meses

   *Respuesta B: Como una cota inferior: el corpus puede incluir algo de texto posterior, así que la evaluación honesta empieza después del corte efectivo estimado y con margen.* En los datos, `modelo-b` se entrenó con texto hasta el 2023-09-30. Las preguntas de la zona gris aciertan un 76 %. Con datos reales no hay verdad publicada: se estima el corte con el propio acierto o con sondas de memoria y se deja margen.

3. Fuera de su corte, `modelo-c` acierta el 66,7 % de 78 preguntas (IC del 95 %: 55 %-77 %, p = 0,01 frente a responder siempre «sí»). La verdad del generador dice que fuera de su memoria los modelos aciertan un 52-55 %. ¿Qué ha pasado?

   - A. El modelo tiene una habilidad real que el generador no recoge
   - B. Hay un error en el intervalo de confianza
   - C. El modelo sigue recordando
   - D. Una racha favorable en una muestra pequeña: con varios modelos evaluados, que uno salga significativo por azar no es raro, y 78 preguntas dan un intervalo de más de 20 puntos

   *Respuesta D: Una racha favorable en una muestra pequeña: con varios modelos evaluados, que uno salga significativo por azar no es raro, y 78 preguntas dan un intervalo de más de 20 puntos.* Es el problema de las pruebas múltiples del nivel 2 con otra cara. Para distinguir un 55 % de un 50 % con potencia del 80 % hacen falta unas 617 preguntas resueltas después del corte, muchas más de las que hay.

4. Para evitar que el modelo reconozca la empresa, se quita su nombre del prompt. Qwen2.5-0.5B sigue respondiendo «2008» a «un gran banco de inversión de Nueva York se declaró en quiebra un 15 de septiembre, en plena crisis de las hipotecas subprime». ¿Qué enseña?

   - A. Que el contexto identifica el hecho aunque falte el nombre: enmascarar entidades reduce el sesgo, no lo elimina
   - B. Que el modelo es aleatorio
   - C. Que hay que traducir el prompt al inglés
   - D. Que el modelo no conoce Lehman Brothers

   *Respuesta A: Que el contexto identifica el hecho aunque falte el nombre: enmascarar entidades reduce el sesgo, no lo elimina.* Sin el contexto de la crisis, el modelo responde «2023» y falla. Lopez-Lira, Tang y Zhu (2025) documentan lo mismo con modelos grandes: enmascarar nombres y fechas no impide que reconstruyan el dato a partir de poco contexto.

5. ¿Cuál es la forma más fiable de backtestear una estrategia que usa un LLM?

   - A. Pedir al modelo en el prompt que no use información posterior a la fecha de la pregunta
   - B. Usar el modelo más reciente, que es el mejor
   - C. Usar solo preguntas resueltas después del corte efectivo del modelo, o un modelo point-in-time entrenado solo con texto anterior a cada fecha, y comparar con una referencia trivial con intervalo de confianza
   - D. Enmascarar el ticker

   *Respuesta C: Usar solo preguntas resueltas después del corte efectivo del modelo, o un modelo point-in-time entrenado solo con texto anterior a cada fecha, y comparar con una referencia trivial con intervalo de confianza.* Las instrucciones no borran la memoria (Lopez-Lira, Tang y Zhu, 2025, Sarkar y Vafa, 2025). Los modelos point-in-time (TiMaGPT, ChronoBERT y ChronoGPT, DatedGPT) o la evaluación posterior al corte son las vías que propone la literatura.

## Ideas clave

- Un LLM recuerda lo que ocurrió antes de su corte: en los datos, los tres modelos aciertan el 89-93 % de lo resuelto antes de su corte declarado y siguiendo sus respuestas se ganaría un 6,2-6,5 % medio en 21 sesiones (t > 10); después, entre el 51 % y el 70 %.
- El corte declarado no basta: el efectivo puede ser posterior. Una ruptura en el acierto lo estima (`modelo-b`: declarado 2023-04-30, estimado 2023-07-31, verdad 2023-09-30), y la evaluación honesta empieza después y con margen.
- Tras el corte efectivo, ningún modelo supera con claridad a responder siempre «sí» (53,7 %) salvo `modelo-c` con 78 preguntas, una racha que la verdad del generador desmiente. Detectar un 55 % frente a un 50 % exige unas 617 preguntas.
- Enmascarar el nombre y la fecha reduce el reconocimiento pero no lo elimina: el contexto, las cifras y el estilo identifican el hecho (Qwen2.5-0.5B responde «2008» a una descripción de la quiebra de Lehman sin el nombre).
- La fecha de corte que dice el propio modelo no es una fuente: Qwen2.5-0.5B responde «Marzo 2019» en español y «October 2023» en inglés.
- Las defensas de la literatura: evaluar solo después del corte (Lopez-Lira y Tang), anonimizar (Glasserman y Lin) y modelos point-in-time (TiMaGPT, ChronoBERT, DatedGPT).

## Recursos

### Libros

- [Glasserman, P. y Lin, C. (2023): Assessing Look-Ahead Bias in Stock Return Predictions Generated By GPT Sentiment Analysis. arXiv:2309.17322](https://arxiv.org/abs/2309.17322) · arxiv.org
- [Lopez-Lira, A., Tang, Y. y Zhu, M. (2025): The Memorization Problem: Can We Trust LLMs' Economic Forecasts? arXiv:2504.14765](https://arxiv.org/abs/2504.14765) · arxiv.org
- [He, S., Lv, L., Manela, A. y Wu, J. (2025): Chronologically Consistent Large Language Models. arXiv:2502.21206](https://arxiv.org/abs/2502.21206) · arxiv.org
- [Yan, Y. et al. (2026): DatedGPT: Preventing Lookahead Bias in Large Language Models with Time-Aware Pretraining. arXiv:2603.11838](https://arxiv.org/abs/2603.11838) · arxiv.org
- [Sarkar, S. y Vafa, K. (2025): Lookahead Bias in Pretrained Language Models (ICML 2025, taller)](https://icml.cc/virtual/2025/50857) · icml.cc
- [Drinkall, F. et al. (2024): Time Machine GPT. arXiv:2404.18543](https://arxiv.org/abs/2404.18543) · arxiv.org

### Datos

- [Datos del nivel 6: banco de preguntas sobre el sesgo de anticipación (CSV)](https://pythonparatrading.com/curso/datos/nivel-6/sesgo6_preguntas.csv) · Descarga · sesgo6_preguntas.csv

Lección anterior: [6.2.4 · Fine-tuning ligero para tareas financieras: LoRA y QLoRA](https://pythonparatrading.com/curso-python-trading/nivel-6/fine-tuning-ligero-lora-y-qlora/)

Lección siguiente: [6.3.1 · Embeddings y bases vectoriales: FAISS, Chroma, pgvector y Qdrant](https://pythonparatrading.com/curso-python-trading/nivel-6/embeddings-y-bases-vectoriales/)
