Lección 6.2.5 · 60 min
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.
- Módulo
- 6.2 · LLMs para investigación y trading
- Duración
- 60 min
Lecciones del nivel 6
6.1 · NLP financiero clásico y moderno
6.2 · LLMs para investigación y trading
6.3 · RAG financiero
6.4 · Agentes de IA para trading
- 6.4.1 Agentes con herramientas: tool calling, ReAct y planificación
- 6.4.2 Marcos de agentes y MCP
- 6.4.3 Arquitecturas multiagente tipo TradingAgents
- 6.4.4 Evidencia y límites de los agentes
- 6.4.5 Guardarraíles para agentes: el agente propone, el motor decide
- 6.4.6 Agentes de investigación: hipótesis, alpha mining y revisión
6.5 · IA como copiloto del quant
6.6 · Big data para trading
- 6.6.1 Cuándo los datos son «big»: ticks, libros, opciones y universos
- 6.6.2 Parquet, Arrow, Polars en streaming y DuckDB fuera de memoria
- 6.6.3 Dask, Spark y Ray: de una máquina a un clúster
- 6.6.4 Bases de series temporales: TimescaleDB, QuestDB, ClickHouse, ArcticDB y kdb+
- 6.6.5 Almacenamiento en la nube, lakehouse (Delta, Iceberg) y GPU (RAPIDS, cuDF)
6.7 · Datos alternativos
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.
Fuente 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.
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"]) 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: noEl 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.
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} %")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
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)) 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.22Los 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 : el rendimiento de saber el futuro.
Después, modelo-a cae al 50,9 % y a un 0,24 % por operación (). 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 % (), menos que estar siempre comprado.
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:
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())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 44El 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):
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")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ésPara 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.
La confianza delata la memoria (en estos datos)
Los modelos simulados declaran una confianza con cada respuesta. En los datos, recordar se nota:
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%}") 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.
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}")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.93modelo-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 %:
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")acierto real 52% frente a 50 %: 3863 preguntas
acierto real 55% frente a 50 %: 617 preguntas
acierto real 60% frente a 50 %: 153 preguntasDistinguir 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.
Fuente 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:
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"]))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 blog1Nota 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.) es pequeño, pero recuerda hechos famosos. La quiebra de Lehman Brothers fue el 15 de septiembre de 20082Nota 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.:
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}")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
- Repite la evaluación honesta de
modelo-busando el corte declarado en vez del efectivo, con margen de 0 días. ¿Cuánto se infla el acierto?
Solución
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}")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.377Con 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.
- Amplía
enmascararpara 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
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))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).
- 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.
Footnotes
-
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. ↩
-
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. ↩
Respuesta plegada en cada pregunta
Autoevaluación
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 %?
- ALo que el modelo recuerda de su corpus de entrenamiento, no su capacidad de predecir
- BLa capacidad del modelo en mercados tranquilos
- CQue el modelo se ha degradado con el tiempo
- DUn buen modelo que pierde capacidad por un cambio de régimen en 2022
Ver respuesta
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.
modelo-bdeclara 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?- AComo exacta, porque la publica el proveedor
- BComo 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
- CComo una cota superior: el modelo sabe menos de lo que dice
- DDa igual, porque la diferencia es de tres meses
Ver respuesta
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-bse 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.Fuera de su corte,
modelo-cacierta 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?- AEl modelo tiene una habilidad real que el generador no recoge
- BHay un error en el intervalo de confianza
- CEl modelo sigue recordando
- DUna 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
Ver respuesta
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.
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?
- AQue el contexto identifica el hecho aunque falte el nombre: enmascarar entidades reduce el sesgo, no lo elimina
- BQue el modelo es aleatorio
- CQue hay que traducir el prompt al inglés
- DQue el modelo no conoce Lehman Brothers
Ver respuesta
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.
¿Cuál es la forma más fiable de backtestear una estrategia que usa un LLM?
- APedir al modelo en el prompt que no use información posterior a la fecha de la pregunta
- BUsar el modelo más reciente, que es el mejor
- CUsar 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
- DEnmascarar el ticker
Ver respuesta
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-ccon 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
- Lopez-Lira, A., Tang, Y. y Zhu, M. (2025): The Memorization Problem: Can We Trust LLMs' Economic Forecasts? arXiv:2504.14765
- He, S., Lv, L., Manela, A. y Wu, J. (2025): Chronologically Consistent Large Language Models. arXiv:2502.21206
- Yan, Y. et al. (2026): DatedGPT: Preventing Lookahead Bias in Large Language Models with Time-Aware Pretraining. arXiv:2603.11838
- Sarkar, S. y Vafa, K. (2025): Lookahead Bias in Pretrained Language Models (ICML 2025, taller)
- Drinkall, F. et al. (2024): Time Machine GPT. arXiv:2404.18543