Lección 5.8.2 · 70 min

Las familias de modelos fundacionales: arquitectura, datos y licencia

En dos años han salido una docena de modelos fundacionales de series de Amazon, Google, Salesforce, Nixtla, NX-AI y grupos académicos. Se parecen más de lo que sugieren sus nombres: casi todos son transformers sobre parches que devuelven cuantiles. Esta lección los ordena por arquitectura, datos de preentrenamiento, tamaño y licencia, con las cifras de sus artículos y fichas, y pone a Chronos-Bolt y TimesFM-2.5 a prever lo mismo; del resto, el código real queda sin ejecutar.

Fig. — Tamaño de los modelos fundacionales de series citados en la lección, según sus artículos y fichas (los dos del curso, contados en los pesos de la caché)
Lecciones del nivel 5
  1. 5.1 · Fundamentos de ML aplicados a finanzas

    1. 5.1.1 Por qué el machine learning financiero es distinto
    2. 5.1.2 Aprendizaje supervisado, no supervisado y por refuerzo; pipelines de scikit-learn
    3. 5.1.3 Métricas económicas para modelos de trading: más allá del acierto
  2. 5.2 · Ingeniería de características

    1. 5.2.1 Features de precio, volumen y microestructura
    2. 5.2.2 Fundamentales point-in-time, macro y datos alternativos
    3. 5.2.3 Diferenciación fraccional para features
    4. 5.2.4 Normalización sin leakage y rank transversal
    5. 5.2.5 Importancia de features: MDI, MDA, SFI y SHAP
  3. 5.3 · Etiquetado y muestreo

    1. 5.3.1 Etiquetado de horizonte fijo y sus problemas
    2. 5.3.2 Método de la triple barrera: etiquetas que imitan una operación
    3. 5.3.3 Meta-labeling: un modelo para el lado y otro para el tamaño
    4. 5.3.4 Unicidad de las etiquetas, pesos por solapamiento y bootstrap secuencial
    5. 5.3.5 Muestreo por eventos: el filtro CUSUM simétrico
  4. 5.4 · Validación para series financieras

    1. 5.4.1 Por qué falla el k-fold aleatorio en datos financieros
    2. 5.4.2 Purged k-fold con embargo y CPCV para modelos de ML
    3. 5.4.3 Walk-forward con reentrenamiento: ventanas y frecuencia
    4. 5.4.4 Backtest de señales de ML con costes y Deflated Sharpe
  5. 5.5 · Modelos clásicos

    1. 5.5.1 Regresiones regularizadas: Ridge, Lasso, Elastic Net y logística
    2. 5.5.2 Árboles de decisión, Random Forest y bagging
    3. 5.5.3 Gradient boosting: XGBoost, LightGBM y CatBoost
    4. 5.5.4 Optuna y el riesgo de sobreajustar la validación
    5. 5.5.5 Aprendizaje no supervisado: PCA, clustering y autoencoders
    6. 5.5.6 Learning to rank para la selección de acciones
  6. 5.6 · Deep learning con Keras 3

    1. 5.6.1 Keras 3: un modelo, varios motores de cálculo
    2. 5.6.2 Un MLP sobre un panel de acciones: regularizar y comparar
    3. 5.6.3 CNN 1D, LSTM y GRU: ventanas, escalado y fugas
    4. 5.6.4 Callbacks, una pérdida de Sharpe y el modelo en producción
  7. 5.7 · Deep learning con PyTorch

    1. 5.7.1 PyTorch: tensores, autograd y el bucle de entrenamiento
    2. 5.7.2 Arquitecturas de series: TCN, N-BEATS, PatchTST e iTransformer
    3. 5.7.3 Modelos probabilísticos: pérdida cuantílica, DeepAR e intervalos
    4. 5.7.4 Redes de grafos para relaciones entre activos
    5. 5.7.5 Optimización de carteras end-to-end: capas diferenciables y pérdida de Sharpe
  8. 5.8 · Modelos fundacionales de series temporales

    1. 5.8.1 Modelos fundacionales de series: preentrenar una vez, prever sin entrenar
    2. 5.8.2 Las familias de modelos fundacionales: arquitectura, datos y licencia
    3. 5.8.3 Uso práctico: contexto, cuantiles, covariables y ajuste fino
    4. 5.8.4 Modelos fundacionales en finanzas: qué dice la evidencia
    5. 5.8.5 Laboratorio: modelos fundacionales frente a GARCH, HAR y LightGBM
  9. 5.9 · Aprendizaje por refuerzo

    1. 5.9.1 MDP, Q-learning, PPO y entornos Gymnasium
    2. 5.9.2 RL para ejecución, market making y cobertura dinámica
    3. 5.9.3 Por qué el RL sobreajusta en trading direccional
  10. 5.10 · Interpretabilidad, robustez y riesgo de modelo

    1. 5.10.1 SHAP, dependencia parcial y estabilidad de las importancias
    2. 5.10.2 Deriva de datos y de concepto: detectarla y qué hacer
    3. 5.10.3 Ensembles, combinación de señales y datos sintéticos

Un mapa de familias

La lección 5.8.1 explicó lo común: preentrenar una vez con un corpus enorme y prever zero-shot a partir de un contexto tipificado y cortado en parches. Lo que distingue a cada familia es la arquitectura del transformer, qué datos vio, qué devuelve, cuánto mide y con qué licencia se publica. La tabla resume lo que dicen sus artículos y fichas (las de Hugging Face y los README de sus repositorios, consultados en octubre de 2026):

ModeloQuién y cuándoArquitecturaSalidaTamañoLicencia de los pesos
ChronosAmazon, mar. 2024T5 codificador-decodificador, un token por datomuestras de trayectorias20-710 MApache-2.0
Chronos-BoltAmazon, nov. 2024T5 codificador-decodificador, parches de 169 cuantiles, 64 pasos de una vez9-205 MApache-2.0
Chronos-2Amazon, oct. 2025T5 solo codificador, atención de tiempo y de grupo21 cuantiles, covariables120 MApache-2.0
TimesFM-2.5Google, sep. 2025solo decodificador, parches de 32media y 9 cuantiles200 MApache-2.0
Moirai-2.0Salesforce, nov. 2025solo decodificador, un tamaño de parchecuantiles, varios tokens por paso11,4 M (small)CC-BY-NC-4.0
TiRexNX-AI, may. 2025xLSTM (sLSTM), no transformercuantiles35 Mcomunitaria NXAI
TimeGPT-1Nixtla, oct. 2023transformer codificador-decodificadorpuntual e intervalos conformesno publicadocerrado (API de pago)
Kronosacadémico, ago. 2025solo decodificador sobre velas OHLCV tokenizadasvelas futuras muestreadas4,1-102,3 M abiertosMIT

La figura de la cabecera pone los tamaños en escala logarítmica. Van de 11 a 710 millones de parámetros, diminutos frente a un modelo de lenguaje, y el más pequeño de la figura, Moirai-2.0 small, es el que sus autores defienden: en su artículo entrenan variantes de 8 y 30 veces más parámetros y ninguna lo mejora en el benchmark GIFT-Eval («we do not observe scaling benefits»). Con los datos de preentrenamiento que hay, más parámetros no compran más precisión.

La cronología cuenta otra historia, la de la velocidad del campo: casi todas las familias sacan una segunda versión en menos de dos años, y la última de TimesFM, la 3.0 de agosto de 2026, cambia la licencia de los pesos.

Fig. — Cronología de los modelos fundacionales de series de la lección, por familia (fecha del primer artículo en arXiv o del anuncio en el repositorio)
ChronosChronos-BoltChronos-2TimesFMTimesFM-2.5TimesFM 3.0MoiraiMoirai-2.0
Fuente Mermaid
flowchart TB
  C[Chronos] --> B[Chronos-Bolt]
  B --> C2[Chronos-2]
  T1[TimesFM] --> T25[TimesFM-2.5]
  T25 --> T3[TimesFM 3.0]
  M1[Moirai] --> M2[Moirai-2.0]

Chronos: del token al parche

El Chronos original (Ansari et al., 2024) es el que la lección 5.8.1 desmontó: escala media, 4 093 cajones, un T5 que predice el token siguiente con entropía cruzada y trayectorias muestreadas. Se preentrenó con 28 conjuntos públicos (unos 84 000 millones de observaciones), 10 millones de series aumentadas con TSMixup (mezclas de series) y 1 millón de series sintéticas de procesos gaussianos (KernelSynth). Sus pesos no están en la caché del curso; el código, con la misma librería, sería este:

IN [01]python
from chronos import ChronosPipeline

chronos = ChronosPipeline.from_pretrained(
    "amazon/chronos-t5-small", device_map="cpu", dtype=torch.float32)
# 20 trayectorias muestreadas: (series, muestras, horizonte)
muestras = chronos.predict(torch.tensor(series["indice"][-512:]),
                           prediction_length=5, num_samples=20)
q10, q50, q90 = np.quantile(muestras[0].numpy(), [0.1, 0.5, 0.9],
                            axis=0)

Chronos-Bolt (noviembre de 2024) cambia el token por el parche y el muestreo por los cuantiles directos. Según su ficha, se entrenó con «casi 100 000 millones de observaciones» y es hasta 250 veces más rápido y 20 veces más eficiente en memoria que el Chronos original del mismo tamaño; el README del repositorio añade un 5 % menos de error. Es el modelo de la lección 5.8.1. Para comparar, primero los datos: los mismos de la lección anterior, en dos activos, con un origen de cada diez sesiones para que TimesFM, más lento, quepa en el tiempo de la lección.

IN [02]python
import numpy as np
import pandas as pd
import torch

torch.manual_seed(0)
torch.use_deterministic_algorithms(True)
vol = pd.read_parquet("vol5_diario.parquet")
series = {}
for a in ("indice", "cripto"):
    d = vol[vol["activo"] == a].set_index("fecha")
    series[a] = np.log(d["rv_5min"].to_numpy())
fechas = pd.DatetimeIndex(d.index)
C = 512
origenes = np.flatnonzero(fechas.year >= 2018)[::10]
print(f"{len(origenes)} orígenes por activo, del "
      f"{fechas[origenes[0]].date()} al {fechas[origenes[-1]].date()}")
OUT [02]Dos series y 202 orígenes de prueba por activo
202 orígenes por activo, del 2018-01-02 al 2025-12-31
IN [03]python
from chronos import BaseChronosPipeline

bolt = BaseChronosPipeline.from_pretrained(
    "amazon/chronos-bolt-small", device_map="cpu",
    dtype=torch.float32)
n_bolt = sum(p.numel() for p in bolt.model.parameters())
print(f"Chronos-Bolt-small: {n_bolt / 1e6:.1f} M parámetros, "
      f"T5 de {bolt.model.config.num_layers} + "
      f"{bolt.model.config.num_decoder_layers} capas, "
      f"d_model {bolt.model.config.d_model}")

def prev_bolt(y):
    v = np.stack([y[i - C:i] for i in origenes])
    q, _ = bolt.predict_quantiles(
        inputs=torch.tensor(v, dtype=torch.float32),
        prediction_length=1)
    return q[:, 0, :].numpy().astype(float)     # cuantiles 0,1…0,9

q_bolt = {a: prev_bolt(y) for a, y in series.items()}
OUT [03]Chronos-Bolt-small: tamaño y previsiones
Chronos-Bolt-small: 47.7 M parámetros, T5 de 6 + 6 capas, d_model 512

47,7 millones de parámetros contados en los pesos, los «48 M» de la ficha: un T5 pequeño con seis capas en el codificador y seis en el decodificador.

Chronos-2: covariables y atención de grupo

Chronos-2 (octubre de 2025) es el salto conceptual de la familia. Según su artículo, es un transformer solo codificador de 120 millones de parámetros que alterna dos tipos de atención: la de tiempo, entre parches de una misma serie, y la de grupo, entre series relacionadas y covariables del mismo ejemplo. Con eso hace, sin reentrenar, previsión univariante, multivariante y con covariables pasadas o conocidas en el futuro (numéricas o categóricas); llega a un contexto de 8 192 pasos, a un horizonte de 1 024 y devuelve 21 cuantiles (del 0,01 al 0,99). Sus autores atribuyen esas capacidades a un preentrenamiento con datos sintéticos que imponen estructuras multivariantes sobre series univariantes. En su benchmark propio (fev-bench) queda primero, por delante de TiRex y TimesFM-2.5.

La librería instalada (chronos-forecasting 2.3.2) trae Chronos2Pipeline, pero los pesos de amazon/chronos-2 no están en la caché del entorno, así que su código va sin ejecutar. La forma de las entradas es la de su documentación (un diccionario por serie); la lección 5.8.3 lo usa con covariables:

IN [04]python
from chronos import Chronos2Pipeline

chronos2 = Chronos2Pipeline.from_pretrained("amazon/chronos-2",
                                            device_map="cpu")
entradas = [{"target": series[a][-2048:]} for a in series]
cuantiles, media = chronos2.predict_quantiles(
    entradas, prediction_length=1, quantile_levels=[0.1, 0.5, 0.9])
# una lista: un tensor por serie (variables, horizonte, cuantiles)

TimesFM-2.5: solo decodificador

TimesFM (Das et al., 2024, Google) es un transformer solo decodificador: lee el contexto en parches de 32 puntos con atención causal, como un modelo de lenguaje, y cada token de salida predice un bloque de 128 pasos. Se preentrenó con del orden de 100 000 millones de puntos de Google Trends, visitas de Wikipedia y series sintéticas. La versión 2.5 (septiembre de 2025) baja de 500 a 200 millones de parámetros, sube el contexto de 2 048 a 16 384 pasos, añade una cabeza opcional de cuantiles continuos de unos 30 millones de parámetros (hasta 1 024 pasos) y quita el indicador de frecuencia que pedía la versión anterior, según el README del proyecto. Su ficha añade al corpus el conjunto de preentrenamiento de GIFT-Eval. Los pesos de la caché permiten comprobar las cifras:

IN [05]python
import timesfm

tfm = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
    "google/timesfm-2.5-200m-pytorch", torch_compile=False)
for nombre, m in tfm.model.named_children():
    n = sum(p.numel() for p in m.parameters())
    print(f"{nombre:<28} {n / 1e6:6.1f} M")
n_tfm = sum(p.numel() for p in tfm.model.parameters())
cfg = tfm.model.config
print(f"{'total':<28} {n_tfm / 1e6:6.1f} M | contexto máx. "
      f"{cfg.context_limit}, parche {cfg.input_patch_len}, salida "
      f"{cfg.output_patch_len}, cuantiles hasta "
      f"{cfg.output_quantile_len}")
OUT [05]TimesFM-2.5: tamaño por bloques y configuración
tokenizer                       1.8 M
stacked_xf                    196.7 M
output_projection_point         4.9 M
output_projection_quantiles    27.9 M
total                         231.3 M | contexto máx. 16384, parche 32, salida 128, cuantiles hasta 1024

El transformer (stacked_xf) y sus proyecciones de entrada y salida puntual suman unos 203 millones, los «200 M» del nombre; la cabeza de cuantiles continuos añade 27,9 millones, la cabeza de unos 30 millones del README. En total, 231,3 millones de parámetros.

Antes de prever hay que compilar la configuración: el contexto máximo, el horizonte máximo y varias opciones. Dos de ellas importan aquí. torch_compile=False evita torch.compile, que en CPU necesita un compilador de C (README del entorno ml). Y force_flip_invariance, activada por defecto, ejecuta el modelo sobre la serie y sobre la serie cambiada de signo y combina las dos; garantiza una propiedad que la sección de equivarianza comprueba, a costa de doblar el cómputo. La lección la desactiva para la comparación (en una prueba previa, con ella activada el ECM cambió en la tercera cifra decimal):

IN [06]python
def compilar(flip):
    tfm.compile(timesfm.ForecastConfig(
        max_context=C, max_horizon=128, normalize_inputs=True,
        use_continuous_quantile_head=True, fix_quantile_crossing=True,
        force_flip_invariance=flip, per_core_batch_size=64))

compilar(flip=False)

def prev_tfm(y):
    punto, q = tfm.forecast(horizon=1,
                            inputs=[y[i - C:i] for i in origenes])
    return punto[:, 0], q[:, 0, :]     # q: media y deciles 0,1…0,9

r_tfm = {a: prev_tfm(y) for a, y in series.items()}
punto, q = r_tfm["indice"]
print("columnas de cuantiles:", q.shape[1],
      "| ¿punto = cuantil 0,5?", np.allclose(punto, q[:, 5]))
OUT [06]TimesFM-2.5: previsiones con los mismos orígenes
columnas de cuantiles: 10 | ¿punto = cuantil 0,5? True

La salida de cuantiles tiene diez columnas, la media y los deciles del 0,1 al 0,9, y la previsión puntual que devuelve forecast es la del cuantil 0,5, la mediana. Las dos familias comparan, por tanto, lo mismo: medianas. Otro detalle: per_core_batch_size es el tamaño del lote; por defecto vale 1 y TimesFM prevé serie a serie.

Cara a cara sobre la volatilidad

Mismos orígenes, mismo contexto de 512 sesiones, mismo objetivo (la log RV de mañana). Además del error cuadrático y el R², la tabla da la pérdida pinball media de los nueve deciles (la regla de puntuación de la lección 5.7.3: premia la distribución entera, no solo la mediana) y la cobertura del intervalo del 10 al 90 %.

IN [07]python
tau = np.arange(1, 10) / 10

def pinball(y, q):
    u = y[:, None] - q
    return np.maximum(tau * u, (tau - 1) * u).mean()

filas = {}
for a, y in series.items():
    real = y[origenes]
    cand = {"ingenua": (y[origenes - 1], None),
            "Chronos-Bolt": (q_bolt[a][:, 4], q_bolt[a]),
            "TimesFM-2.5": (r_tfm[a][0], r_tfm[a][1][:, 1:])}
    for m, (f, qs) in cand.items():
        fila = {"ECM": np.mean((real - f) ** 2),
                "R²": np.corrcoef(real, f)[0, 1] ** 2}
        if qs is not None:
            fila["pinball"] = pinball(real, qs)
            fila["cob. 10-90"] = np.mean((real >= qs[:, 0])
                                         & (real <= qs[:, 8]))
        filas[(a, m)] = fila
tabla = pd.DataFrame(filas).T
tabla.round(3)
OUT [07]Las dos familias frente a la ingenua
                       ECM     R²  pinball  cob. 10-90
indice ingenua       0.241  0.511      NaN         NaN
       Chronos-Bolt  0.205  0.543    0.140       0.762
       TimesFM-2.5   0.210  0.532    0.141       0.782
cripto ingenua       0.621  0.303      NaN         NaN
       Chronos-Bolt  0.370  0.514    0.167       0.807
       TimesFM-2.5   0.378  0.505    0.169       0.787
Fig. — Error cuadrático medio de Chronos-Bolt-small y TimesFM-2.5 zero-shot en la previsión a un día de la log RV, relativo al de la previsión ingenua, en 202 orígenes de 2018-2025

Los dos modelos mejoran a la ingenua y quedan muy juntos: en el índice, ECM de 0,205 (Bolt) y 0,210 (TimesFM) frente a 0,241; en cripto, 0,370 y 0,378 frente a 0,621. La pinball también es casi igual (0,140 frente a 0,141 en el índice) y la cobertura del 10-90 % va del 76 al 81 %, alrededor del 80 % nominal. Las cifras del índice no coinciden con las de la lección 5.8.1 porque aquí se usa un origen de cada diez (202 días en lugar de 2 011). Con tan pocos días, las diferencias hay que medirlas:

IN [08]python
def pareada(y, f1, f2):
    real = y[origenes]
    d = (real - f1) ** 2 - (real - f2) ** 2
    t = d.mean() / d.std(ddof=1) * np.sqrt(len(d))
    return f"{d.mean():+.4f} (t = {t:+.1f})"

for a, y in series.items():
    fb, ft = q_bolt[a][:, 4], r_tfm[a][0]
    print(f"{a:<7} Bolt − ingenua {pareada(y, fb, y[origenes - 1])}  "
          f"TimesFM − ingenua {pareada(y, ft, y[origenes - 1])}  "
          f"TimesFM − Bolt {pareada(y, ft, fb)}")
OUT [08]Diferencias pareadas de error cuadrático
indice  Bolt − ingenua -0.0366 (t = -2.0)  TimesFM − ingenua -0.0316 (t = -1.7)  TimesFM − Bolt +0.0050 (t = +1.0)
cripto  Bolt − ingenua -0.2503 (t = -2.6)  TimesFM − ingenua -0.2423 (t = -2.6)  TimesFM − Bolt +0.0080 (t = +1.2)

Frente a la ingenua, t entre −1,7 y −2,6 con 202 días (en la lección 5.8.1, con todos los días del índice, Bolt daba t = −9,8: la muestra manda). Entre los dos modelos, t = 1,0 y 1,2: no se distinguen. Un modelo de 47,7 millones de parámetros y otro de 231,3 millones, de dos empresas, con datos y arquitecturas distintos, dan aquí casi la misma previsión. Es lo esperable en una serie con una estructura sencilla (un HAR en logaritmos): cualquier modelo competente extrae casi toda la información del contexto, y el resto es ruido que nadie puede prever.

Equivarianza: lo que la arquitectura garantiza

Una propiedad útil de un modelo de previsión: si se multiplica la serie por aa y se le suma bb, la previsión debería transformarse igual, f(ax+b)=a f(x)+bf(ax + b) = a\,f(x) + b. Si no la cumple, el resultado depende de las unidades (porcentaje o tanto por uno, dólares o euros). Los dos modelos tipifican el contexto antes de procesarlo, así que la cumplen por construcción para a>0a > 0. Para a<0a < 0 (darle la vuelta a la serie) no hay garantía: la serie tipificada cambia de forma.

IN [09]python
x = series["indice"][-C:]

def una_bolt(v):
    q, _ = bolt.predict_quantiles(
        inputs=torch.tensor(v, dtype=torch.float32),
        prediction_length=1)
    return q[0, 0, 4].item()

def una_tfm(v):
    return tfm.forecast(horizon=1, inputs=[v])[0][0, 0]

for nombre, f in (("Bolt", una_bolt), ("TimesFM, sin flip", una_tfm)):
    base = f(x)
    e1 = abs(f(2 * x + 3) - (2 * base + 3))
    e2 = abs(f(-x) - (-base))
    print(f"{nombre:<18} |f(2x+3) − (2f(x)+3)| = {e1:.4f}   "
          f"|f(−x) − (−f(x))| = {e2:.4f}")
compilar(flip=True)
base = una_tfm(x)
print(f"{'TimesFM, con flip':<18} |f(−x) − (−f(x))| = "
      f"{abs(una_tfm(-x) + base):.4f}")
OUT [09]Equivarianza: escalar y desplazar la serie, y darle la vuelta
Bolt               |f(2x+3) − (2f(x)+3)| = 0.0000   |f(−x) − (−f(x))| = 0.0931
TimesFM, sin flip  |f(2x+3) − (2f(x)+3)| = 0.0000   |f(−x) − (−f(x))| = 0.0982
TimesFM, con flip  |f(−x) − (−f(x))| = 0.0000

Escalar por 2 y sumar 3 no cambia nada en ninguno de los dos. Darle la vuelta a la serie, sí: unas 0,09 unidades de log RV en los dos, del orden de un 10 % de la varianza. La volatilidad tiene picos hacia arriba y no hacia abajo; la serie invertida tiene valles en su lugar, y el modelo no tiene por qué tratarla como el espejo de la original. Con force_flip_invariance=True, TimesFM promedia f(x) y −f(−x), y la propiedad se cumple exacta. Para una serie con signo (rendimientos, diferencias de tipos) puede ser deseable; para la volatilidad, que solo tiene picos hacia arriba, no está claro que lo sea, y cuesta el doble.

Moirai-2.0, TiRex, TimeGPT y Kronos

Las otras cuatro familias del temario no se ejecutan en el curso, cada una por un motivo (README del entorno ml). Su código es el de su documentación oficial.

Moirai (Woo et al., 2024, Salesforce) se presentó como un transformer de codificador enmascarado con varios tamaños de parche, atención «any-variate» (cualquier número de variables en una misma secuencia) y una salida de mezcla de distribuciones (t de Student, log-normal, binomial negativa, normal de varianza baja), preentrenado con LOTSA, más de 27 000 millones de observaciones de nueve dominios. Moirai-2.0 (noviembre de 2025) lo simplifica todo: solo decodificador, un único tamaño de parche, pérdida cuantílica y varios tokens de salida por paso, con un corpus nuevo de 36 millones de series (unos 295 000 millones de observaciones). Sus autores lo describen como el doble de rápido y 30 veces más pequeño que Moirai 1.0-Large, y mejor. Su librería, uni2ts, exige numpy~=1.26 y torch<2.5, que no existen para Python 3.14, y sus pesos son CC-BY-NC-4.0, solo para investigación. El código del README:

IN [10]python
from uni2ts.model.moirai2 import Moirai2Forecast, Moirai2Module

modelo = Moirai2Forecast(
    module=Moirai2Module.from_pretrained(
        "Salesforce/moirai-2.0-R-small"),
    prediction_length=100,
    context_length=1680,
    target_dim=1,
    feat_dynamic_real_dim=0,
    past_feat_dynamic_real_dim=0,
)
predictor = modelo.create_predictor(batch_size=32)
# datos_gluonts: entradas de un PandasDataset de GluonTS
previsiones = predictor.predict(datos_gluonts)

TiRex (Auer et al., 2025, NX-AI; NeurIPS 2025) es la excepción arquitectónica: no es un transformer sino una xLSTM (en concreto, con bloques sLSTM), una red recurrente moderna que según sus autores conserva la capacidad de seguir el estado de la serie, útil en horizontes largos. Tiene 35 millones de parámetros, se entrenó con 47,5 millones de series (de Chronos, aumentadas con TSMixup, sintéticas y parte del preentrenamiento de GIFT-Eval) y en su artículo supera a modelos mayores en GIFT-Eval y en el benchmark de Chronos. Su licencia es la NXAI community license: una empresa con más de 100 millones de euros de facturación anual que lo integre en un producto o servicio comercial necesita una licencia comercial. Se instala con pip install tirex-ts; no está en el entorno del curso. El ejemplo de su README:

IN [11]python
from tirex import load_model, ForecastModel

modelo: ForecastModel = load_model("NX-AI/TiRex")
contexto = torch.tensor(np.stack([series[a][-512:] for a in series]))
cuantiles, media = modelo.forecast(context=contexto,
                                   prediction_length=1)

TimeGPT-1 (Garza, Challu y Mergenthaler-Canseco, 2023, Nixtla) fue de los primeros: un transformer codificador-decodificador preentrenado con «más de 100 000 millones de puntos». Es cerrado: no hay pesos, se usa mediante una API de pago con el SDK nixtla (Apache-2.0) y sus intervalos salen de predicción conforme sobre los errores históricos, no del modelo. Para un fondo, el coste no es solo la cuota: la serie viaja a un servidor ajeno. La firma es la de la referencia del SDK (la clave, de una variable de entorno, nunca en el código):

IN [12]python
import os
from nixtla import NixtlaClient

cliente = NixtlaClient(api_key=os.environ["NIXTLA_API_KEY"])
df = pd.DataFrame({"unique_id": "indice", "ds": fechas,
                   "y": series["indice"]})
prevision = cliente.forecast(df=df, h=5, freq="B", level=[80])

Kronos (Shi et al., 2025; aceptado en AAAI 2026) es el único de la lista específico de mercados. Se preentrena con más de 12 000 millones de velas (registros OHLCV) de 45 mercados y siete frecuencias, y su tokenizador convierte cada vela multivariante en dos subtokens jerárquicos (uno grueso y uno fino) con una cuantización esférica binaria; un transformer solo decodificador predice las velas siguientes por muestreo. Hay versiones abiertas de 4,1 (mini), 24,7 (small) y 102,3 millones (base), con un contexto de 512 velas en small y base, y una large de 499,2 millones sin publicar; licencia MIT. No es un paquete de PyPI (el paquete kronos de PyPI es otra cosa): se clona el repositorio y se importa desde su carpeta model. El ejemplo de su README, con las columnas que espera:

IN [13]python
from model import Kronos, KronosTokenizer, KronosPredictor

tokenizer = KronosTokenizer.from_pretrained(
    "NeoQuasar/Kronos-Tokenizer-base")
modelo = Kronos.from_pretrained("NeoQuasar/Kronos-small")
predictor = KronosPredictor(modelo, tokenizer, max_context=512)

columnas = ["open", "high", "low", "close", "volume", "amount"]
x_df = velas.loc[:399, columnas]
pred_df = predictor.predict(
    df=x_df,
    x_timestamp=velas.loc[:399, "timestamps"],
    y_timestamp=velas.loc[400:519, "timestamps"],
    pred_len=120,
    T=1.0,          # temperatura del muestreo
    top_p=0.9,      # muestreo de núcleo
    sample_count=1, # trayectorias que se promedian
)

Sin el argumento device, el código del repositorio elige CUDA, después MPS (Apple) y si no, CPU.

Qué mirar para elegir

CriterioPreguntaEjemplo
Licencia de los pesos¿Permite el uso comercial que tengo en mente?Moirai-2.0 no; TiRex con límites; TimesFM 3.0, pesos no comerciales
Dónde se ejecuta¿Los datos salen de casa?TimeGPT: API externa
Entradas¿Necesito covariables o varias series a la vez?Chronos-2 y TimesFM-2.5 (XReg, lección 5.8.3)
Salida¿Cuantiles, muestras o solo un punto?Bolt y TimesFM: cuantiles; Kronos: muestras de velas
Coste en CPU¿Cuántas previsiones por día y en qué máquina?Bolt-small es varias veces más rápido que TimesFM-2.5 en esta prueba
Dependencias¿Se instala en mi versión de Python?uni2ts exige numpy 1.26 y torch < 2.5
Evidencia¿Gana en mis datos, con una prueba honesta?lecciones 5.8.4 y 5.8.5

Ejercicios

  1. Con las previsiones ya calculadas, mide la calibración cuantil a cuantil de los dos modelos en cripto: la fracción de días en que la realizada queda por debajo de cada cuantil previsto. ¿Cuál se acerca más a la diagonal?
Solución
IN [14]python
real = series["cripto"][origenes]
frec = pd.DataFrame({
    "nominal": tau,
    "Chronos-Bolt": (real[:, None] <= q_bolt["cripto"]).mean(0),
    "TimesFM-2.5": (real[:, None] <= r_tfm["cripto"][1][:, 1:]).mean(0),
})
print(frec.round(3).to_string(index=False))
print("desviación media absoluta:",
      (frec.iloc[:, 1:].sub(frec["nominal"], axis=0)).abs().mean()
      .round(3).to_dict())
OUT [14]Calibración por cuantil en cripto
 nominal  Chronos-Bolt  TimesFM-2.5
     0.1         0.104        0.094
     0.2         0.168        0.173
     0.3         0.277        0.287
     0.4         0.386        0.391
     0.5         0.475        0.505
     0.6         0.559        0.569
     0.7         0.644        0.644
     0.8         0.767        0.752
     0.9         0.911        0.881
desviación media absoluta: {'Chronos-Bolt': 0.026, 'TimesFM-2.5': 0.024}

Las dos columnas siguen la diagonal con una desviación media de 2,6 puntos (Bolt) y 2,4 (TimesFM). Con 202 días, cada frecuencia tiene un error típico de unos 2-3 puntos (√(0,5·0,5/202) ≈ 0,035 en el centro), así que diferencias de ese tamaño entre los dos modelos no dicen nada. La lección 5.8.3 lo repite con todos los días y por tramos de volatilidad, que es donde aparecen los problemas.

  1. TimesFM-2.5 admite contextos de hasta 16 384 pasos. Repite su previsión del índice con un contexto de 128 sesiones (recompila con max_context=128). ¿Cuánto empeora el ECM?
Solución
IN [15]python
tfm.compile(timesfm.ForecastConfig(
    max_context=128, max_horizon=128, normalize_inputs=True,
    use_continuous_quantile_head=True, fix_quantile_crossing=True,
    force_flip_invariance=False, per_core_batch_size=64))
y = series["indice"]
p128, _ = tfm.forecast(horizon=1,
                       inputs=[y[i - 128:i] for i in origenes])
real = y[origenes]
print(f"ECM con 512: {np.mean((real - r_tfm['indice'][0]) ** 2):.3f}"
      f"   con 128: {np.mean((real - p128[:, 0]) ** 2):.3f}")
print("diferencia pareada:",
      pareada(y, p128[:, 0], r_tfm["indice"][0]))
OUT [15]TimesFM con 128 sesiones de contexto
ECM con 512: 0.210   con 128: 0.213
diferencia pareada: +0.0034 (t = +0.5)

Con 128 sesiones el ECM pasa de 0,210 a 0,213 y la diferencia pareada da t = 0,5: no es significativa. En una serie cuya memoria útil es de semanas (el HAR usa medias de 1, 5 y 22 días), medio año de contexto ya contiene casi toda la información para el día siguiente. La lección 5.8.3 barre la longitud del contexto de forma sistemática.

  1. Un fondo con 500 millones de euros de facturación quiere integrar un modelo fundacional en un producto de previsión que venderá a clientes, sin enviar datos fuera. ¿Qué modelos de la lección descartarías por licencia o por despliegue, y cuáles quedarían?
Solución

Descartaría TimeGPT (API externa: los datos salen de casa y el modelo no se puede desplegar dentro), Moirai-2.0 (pesos CC-BY-NC-4.0, «research purposes only») y TimesFM 3.0 (pesos con licencia no comercial). TiRex no queda descartado de entrada, pero la NXAI community license exige una licencia comercial a empresas de más de 100 millones de euros que lo integren en un producto comercial, que es el caso: habría que negociarla. Quedan Chronos-Bolt, Chronos-2 y TimesFM-2.5 (pesos Apache-2.0) y Kronos (MIT). La licencia es la condición necesaria; la suficiente es que el elegido gane en una prueba honesta con los datos del fondo, que es lo que hacen las lecciones 5.8.4 y 5.8.5.

Respuesta plegada en cada pregunta

Autoevaluación

  1. ¿Qué diferencia de arquitectura hay entre Chronos-Bolt y TimesFM-2.5?

    • AChronos-Bolt es una red recurrente y TimesFM una convolución
    • BChronos-Bolt es un T5 codificador-decodificador que produce de una vez los cuantiles del horizonte; TimesFM es un transformer solo decodificador, con atención causal entre parches, que genera parches de salida de 128 pasos
    • CNinguna: son el mismo modelo con otro nombre
    • DTimesFM cuantiza cada dato en un token, como el Chronos original
    Ver respuesta

    Respuesta B. Chronos-Bolt es un T5 codificador-decodificador que produce de una vez los cuantiles del horizonte; TimesFM es un transformer solo decodificador, con atención causal entre parches, que genera parches de salida de 128 pasos

    Los dos trabajan con parches (16 y 32 datos), pero Bolt codifica el contexto y decodifica todos los cuantiles en un paso, y TimesFM lee los parches con atención causal, como un modelo de lenguaje, y predice el siguiente bloque.

  2. En la prueba sobre vol5 (202 orígenes por activo), TimesFM-2.5 tiene un ECM 0,005 mayor que Chronos-Bolt en el índice, con t = 1,0. ¿Qué se concluye?

    • AQue Chronos-Bolt es mejor modelo
    • BQue TimesFM-2.5 está mal configurado
    • CQue hay que usar el modelo con más parámetros
    • DQue con esta muestra no se distinguen: la diferencia es del orden de su error típico, y los dos mejoran a la ingenua de forma parecida
    Ver respuesta

    Respuesta D. Que con esta muestra no se distinguen: la diferencia es del orden de su error típico, y los dos mejoran a la ingenua de forma parecida

    Un t de 1,0 es compatible con que no haya diferencia. Para ordenar dos modelos tan parecidos haría falta mucha más muestra (o más activos), y aun así la conclusión valdría para estos datos.

  3. Moirai-2.0 se publica con licencia CC-BY-NC-4.0 y TiRex con la «NXAI community license». ¿Qué implica para un fondo que quiera usarlos en producción?

    • AMoirai-2.0 no admite uso comercial («research purposes only» en su ficha) y TiRex exige licencia comercial a empresas de más de 100 millones de euros de facturación que lo integren en un producto comercial; Chronos-Bolt, Chronos-2 y TimesFM-2.5 son Apache-2.0
    • BNada: los pesos publicados en Hugging Face son siempre de uso libre
    • CQue hay que pagar a Hugging Face
    • DQue solo se pueden usar en GPU
    Ver respuesta

    Respuesta A. Moirai-2.0 no admite uso comercial («research purposes only» en su ficha) y TiRex exige licencia comercial a empresas de más de 100 millones de euros de facturación que lo integren en un producto comercial; Chronos-Bolt, Chronos-2 y TimesFM-2.5 son Apache-2.0

    La licencia de los pesos es tan importante como la del código. uni2ts (el código de Moirai) es Apache-2.0, pero sus pesos de la versión 2.0 son CC-BY-NC-4.0. Y TimesFM 3.0, de agosto de 2026, ya publica sus pesos con una licencia no comercial.

  4. La celda de equivarianza da |f(2x+3) − (2f(x)+3)| = 0 en los dos modelos, pero |f(−x) − (−f(x))| ≈ 0,09 sin la opción force_flip_invariance. ¿Por qué?

    • APorque los modelos tienen un error de programación
    • BPorque la serie es negativa
    • CPorque los dos tipifican el contexto (restan la media y dividen por la desviación), lo que garantiza la equivarianza para a > 0; darle la vuelta a la serie cambia su forma (la asimetría de los picos), y el modelo no tiene por qué devolver la previsión invertida, salvo que se le obligue promediando f(x) y −f(−x)
    • DPorque el horizonte es de un día
    Ver respuesta

    Respuesta C. Porque los dos tipifican el contexto (restan la media y dividen por la desviación), lo que garantiza la equivarianza para a > 0; darle la vuelta a la serie cambia su forma (la asimetría de los picos), y el modelo no tiene por qué devolver la previsión invertida, salvo que se le obligue promediando f(x) y −f(−x)

    Con la tipificación, f(ax + b) = a·f(x) + b para cualquier a > 0. Con a < 0 la serie tipificada es otra (los picos de volatilidad se vuelven valles). TimesFM lo fuerza con force_flip_invariance=True, que ejecuta el modelo dos veces; por eso la lección lo desactiva para medir.

  5. ¿Qué hace distinto a Kronos frente al resto de familias?

    • AQue no usa transformers
    • BQue es el único con licencia comercial de pago
    • CQue predice solo la volatilidad
    • DQue se preentrena con velas OHLCV (más de 12 000 millones de registros de 45 mercados) y tokeniza cada vela entera, en lugar de una serie univariante cualquiera
    Ver respuesta

    Respuesta D. Que se preentrena con velas OHLCV (más de 12 000 millones de registros de 45 mercados) y tokeniza cada vela entera, en lugar de una serie univariante cualquiera

    Kronos es específico de mercados financieros: su tokenizador convierte la vela multivariante (apertura, máximo, mínimo, cierre, volumen, importe) en tokens jerárquicos. Que sea específico no lo hace capaz de predecir rendimientos: la lección 5.8.4 revisa la evidencia.

Ideas clave

  • Casi todas las familias son transformers sobre parches que devuelven cuantiles: Chronos-Bolt (T5 codificador-decodificador, parches de 16), TimesFM-2.5 (solo decodificador, parches de 32), Chronos-2 (solo codificador, con covariables), Moirai-2.0 (solo decodificador). TiRex usa xLSTM y Kronos tokeniza velas OHLCV.
  • El tamaño no ordena la calidad: Chronos-Bolt-small (47,7 M parámetros contados) iguala aquí a TimesFM-2.5 (231,3 M en el fichero, 200 M según la ficha), y los autores de Moirai-2.0 encuentran que sus versiones grandes rinden menos que la pequeña.
  • En vol5, los dos ejecutables reducen el ECM de la ingenua un 13-15 % en el índice y un 39-40 % en cripto; entre ellos, diferencias de t ≈ 1: no se distinguen.
  • Los dos son equivariantes a escala y desplazamiento positivos por construcción; frente a darle la vuelta a la serie, solo TimesFM con force_flip_invariance=True, que dobla el cómputo.
  • La licencia de los pesos cuenta: Apache-2.0 en Chronos-Bolt, Chronos-2 y TimesFM-2.5; CC-BY-NC-4.0 en Moirai-2.0; comunitaria con límites en TiRex; MIT en Kronos; API de pago en TimeGPT (los datos salen de casa).

Recursos