Lección 5.9.2 · 75 min

RL para ejecución, market making y cobertura dinámica

Donde el aprendizaje por refuerzo tiene argumentos serios en trading es en problemas de control con un simulador creíble y un objetivo claro: liquidar una posición, cotizar como creador de mercado, cubrir una opción. Esta lección entrena un agente en cada uno y lo mide en caminos de prueba fijos contra la solución analítica que ya se conoce. Gana donde la referencia hace supuestos que el simulador no cumple; donde no, como mucho la iguala.

Fig. — Inventario por liquidar en cada minuto al vender 200.000 acciones en 60 minutos: TWAP, la trayectoria óptima de Almgren-Chriss y la que aprende PPO con la recompensa esperada
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

Dónde el RL tiene argumentos

La lección 5.9.1 terminó con una advertencia: un agente solo se puede juzgar si hay algo con qué compararlo. En trading direccional no lo hay (y la lección 5.9.3 enseña lo que pasa). En tres problemas de control, sí:

ProblemaAcciónObjetivoReferencia analíticaLección
Ejecución óptimacuánto vender en cada minutocoste + λ·varianzaAlmgren-Chriss4.3.1
Market makingdónde poner las cotizacionesP&L − riesgo de inventarioAvellaneda-Stoikov2.3.6
Cobertura dinámicacuánto subyacente tenerP&L − λ·varianzadelta de Black-Scholes3.6.2

Los tres tienen en común lo que le falta al trading direccional: un simulador que se puede creer en lo esencial (impacto, llegadas de órdenes, dinámica del subyacente), un objetivo que no depende de predecir el mercado, y una solución de referencia con supuestos conocidos. El agente puede ganar a la referencia donde el simulador no cumple esos supuestos; donde los cumple, lo mejor que puede hacer es igualarla. El método de la lección es siempre el mismo:

SimuladorEntrenar agenteCaminos de pruebaReferenciaDiferencia y error
Fuente Mermaid
flowchart LR
  A[Simulador] --> B[Entrenar agente]
  B --> C[Caminos de prueba]
  D[Referencia] --> C
  C --> E[Diferencia y error]

Los caminos de prueba son fijos y comunes: los mismos para el agente y para las referencias, nunca vistos al entrenar. Están en los datos del nivel 5, con los parámetros y las cifras de referencia que el README ya publica.

Ejecución: la recompensa que suma el objetivo de Almgren-Chriss

El problema es el de la lección 4.3.1 con impacto lineal: vender X = 200.000 acciones a S0 = 50 USD en 60 pasos de un minuto. El medio sigue Sk=Sk−1+στ ξk−τγvkS_k = S_{k-1} + \sigma\sqrt{\tau}\,\xi_k - \tau\gamma v_k (impacto permanente γ\gamma) y la venta del paso kk, nkn_k acciones a ritmo vk=nk/τv_k = n_k/\tau, se ejecuta a Sk−1−ε−ηvkS_{k-1} - \varepsilon - \eta v_k (horquilla e impacto temporal). El coste es el implementation shortfall C=XS0−∑knk⋅preciokC = X S_0 - \sum_k n_k \cdot \text{precio}_k y el objetivo, minimizar E[C]+λVar⁡[C]\mathbb{E}[C] + \lambda \operatorname{Var}[C]. Almgren y Chriss dan la trayectoria óptima de inventario en forma cerrada, xk=Xsinh⁡(κ(T−tk))/sinh⁡(κT)x_k = X \sinh(\kappa(T - t_k)) / \sinh(\kappa T), que está en el JSON.

IN [01]python
import json
import math
import numpy as np
import pandas as pd

cfg = json.load(open("rl5_entornos.json", encoding="utf-8"))
ej = cfg["ejecucion"]
X, N, S0, TAU = ej["X"], ej["N"], ej["S0"], ej["tau"]
SIG, ETA, GAM = ej["sigma"], ej["eta"], ej["gamma"]
EPS, LAM = ej["epsilon"], ej["lam"]
x_ac = np.array(ej["trayectoria_optima"])
x_tw = X * (1 - np.arange(N + 1) / N)

cam = pd.read_parquet("rl5_caminos_ejecucion.parquet")
xi = (cam.pivot(index="camino", columns="paso", values="xi")
      .to_numpy()[:, 1:])  # el paso 0 es S0, sin innovación
print(xi.shape, f"kappa*T = {ej['kappa'] * N:.2f}",
      f"sigma diaria = {SIG * math.sqrt(390) / S0:.1%}")
OUT [01]Parámetros y caminos de prueba de la ejecución
(1000, 60) kappa*T = 2.01 sigma diaria = 2.0%

Antes de entrenar nada, el simulador tiene que reproducir las cifras publicadas. Dos funciones: el coste realizado en unos caminos y el objetivo teórico de una trayectoria determinista (fórmulas de Almgren-Chriss):

IN [02]python
def coste_is(x, xi):
    """Implementation shortfall (USD) de vender según el inventario
    x (N+1 valores) en los caminos de innovaciones xi (m, N)."""
    n = -np.diff(x)
    v = n / TAU
    dS = SIG * math.sqrt(TAU) * xi - TAU * GAM * v
    S = S0 + np.cumsum(dS, axis=1)
    S_ant = np.column_stack([np.full(len(xi), S0), S[:, :-1]])
    return X * S0 - (n * (S_ant - EPS - ETA * v)).sum(axis=1)

def objetivo_teorico(x):
    n = -np.diff(x)
    E = (0.5 * GAM * X**2 + EPS * X
         + (ETA - 0.5 * GAM * TAU) / TAU * (n**2).sum())
    V = SIG**2 * TAU * (x[1:] ** 2).sum()
    return E + LAM * V

def resumen(c):
    return {"media": c.mean(), "desv.": c.std(ddof=1),
            "media + λ·var": c.mean() + LAM * c.var(ddof=1)}

ref = {"TWAP": x_tw, "Almgren-Chriss": x_ac}
tabla = pd.DataFrame({k: resumen(coste_is(x, xi))
                      for k, x in ref.items()}).T
tabla["teórico E + λV"] = [objetivo_teorico(x) for x in ref.values()]
tabla.round(0)
OUT [02]El coste en los caminos de prueba reproduce el README
                  media    desv.  media + λ·var  teórico E + λV
TWAP            31866.0  46054.0        38228.0         36584.0
Almgren-Chriss  32301.0  36915.0        36389.0         35385.0

Las cifras coinciden con las del README (38.228 y 36.389 USD medidos; 36.584 y 35.385 teóricos): el simulador es el mismo. Almgren-Chriss vende más deprisa al principio (paga más impacto) para quitarse antes el riesgo de precio, y gana 1.839 USD de objetivo a TWAP en los caminos de prueba.

Para que un agente optimice ese objetivo, la recompensa de cada paso tiene que sumar exactamente −(C+λV)-(C + \lambda V). Escribiendo el precio de cada venta como S0S_0 más los movimientos anteriores, el coste se reparte por pasos:

C=∑k=1N[nk(ε+ηvk)+xkτγvk−xkστ ξk]C = \sum_{k=1}^{N} \Big[ n_k(\varepsilon + \eta v_k) + x_k \tau \gamma v_k - x_k \sigma \sqrt{\tau}\, \xi_k \Big]

con xkx_k el inventario que queda tras la venta kk. El primer término es el coste de ejecutar; el segundo, el impacto permanente que se come el inventario restante; el tercero, lo que se gana o se pierde por el movimiento del precio con ese inventario. La varianza de una trayectoria determinista es σ2τ∑kxk2\sigma^2\tau\sum_k x_k^2, así que la recompensa del paso kk es

rk=−[nk(ε+ηvk)+xkτγvk+λσ2τxk2]+xkστ ξkr_k = -\Big[ n_k(\varepsilon + \eta v_k) + x_k \tau \gamma v_k + \lambda \sigma^2 \tau x_k^2 \Big] + x_k \sigma \sqrt{\tau}\, \xi_k

El último término es ruido de media cero: ξk\xi_k es independiente de la decisión. Tiene una desviación enorme (con 100.000 acciones en cartera, unos 5.000 USD por minuto) frente a diferencias de unos cientos de USD entre estrategias en todo el episodio. Quitarlo no cambia la esperanza de la recompensa ni la política óptima, y reduce mucho su varianza: es una variable de control, y la penalización λσ2τxk2\lambda\sigma^2\tau x_k^2 ya representa el riesgo. El entorno admite las dos versiones.

IN [03]python
import gymnasium as gym
from gymnasium import spaces
from gymnasium.utils.env_checker import check_env

MULT = np.array([0.5, 0.75, 1.0, 1.5, 2.0, 2.5])

class EjecucionEnv(gym.Env):
    """Observa (fracción del tiempo, fracción del inventario); vende
    MULT[a] veces el ritmo TWAP del inventario que queda."""

    def __init__(self, ruido=True):
        self.ruido = ruido
        self.observation_space = spaces.Box(0.0, 1.0, (2,),
                                            np.float32)
        self.action_space = spaces.Discrete(len(MULT))

    def _obs(self):
        return np.array([self.k / N, self.x / X], np.float32)

    def reset(self, seed=None, options=None):
        super().reset(seed=seed)
        self.k, self.x = 0, float(X)
        return self._obs(), {}

    def step(self, a):
        quedan = N - self.k
        n = self.x if quedan == 1 else min(
            self.x, self.x / quedan * MULT[int(a)])
        v = n / TAU
        self.x -= n
        self.k += 1
        coste = (n * (EPS + ETA * v) + self.x * TAU * GAM * v
                 + LAM * SIG**2 * TAU * self.x**2)
        if self.ruido:
            xi_k = self.np_random.standard_normal()
            coste -= self.x * SIG * math.sqrt(TAU) * xi_k
        return self._obs(), -coste / 1000, self.k == N, False, {}

check_env(EjecucionEnv(), skip_render_check=True)
print("check_env: sin errores")
OUT [03]EjecucionEnv: vender como múltiplo del ritmo TWAP
check_env: sin errores

Decisiones de diseño. La acción es un múltiplo del ritmo TWAP del inventario restante (0,5 a 2,5 veces), así que cualquier política vende todo a tiempo y el último paso liquida lo que quede; Almgren-Chriss empieza vendiendo unas 2,1 veces el ritmo TWAP y termina en 1, dentro del rango. La observación no incluye el precio: en este modelo la trayectoria óptima no depende de él, y dárselo solo le daría al agente ruido al que reaccionar. La recompensa va en miles de USD (escala de orden 1, como en 5.9.1). El episodio termina de verdad al vender todo (terminated), y sin descuento (γ=1\gamma = 1): el coste de un minuto pesa lo mismo al principio que al final.

Ejecución: PPO con y sin el ruido

Primero, con la recompensa realizada, la que vería un agente que solo observa el P&L. PPO con 8 entornos, sin descuento, gae_lambda=1 (la ventaja con el retorno completo del episodio) y tasa de aprendizaje 0,001; tras cada bloque de 30.000 pasos (500 episodios) se lee la trayectoria determinista del agente y se calcula su objetivo teórico:

IN [04]python
import torch
from stable_baselines3 import PPO
from stable_baselines3.common.env_util import make_vec_env

torch.manual_seed(0)
torch.use_deterministic_algorithms(True)

def trayectoria(modelo):
    env = EjecucionEnv(ruido=False)
    o, _ = env.reset(seed=0)
    x = [float(X)]
    for _ in range(N):
        a, _ = modelo.predict(o, deterministic=True)
        o, *_ = env.step(a)
        x.append(env.x)
    return np.array(x)

def entrena_ejecucion(ruido, bloques):
    vec = make_vec_env(lambda: EjecucionEnv(ruido), n_envs=8,
                       seed=0)
    m = PPO("MlpPolicy", vec, gamma=1.0, gae_lambda=1.0,
            n_steps=240, batch_size=240, learning_rate=1e-3,
            policy_kwargs={"net_arch": [32, 32]},
            seed=0, device="cpu", verbose=0)
    for b in range(bloques):
        m.learn(30_000, reset_num_timesteps=b == 0)
        x = trayectoria(m)
        print(f"{30_000 * (b + 1):>6} pasos: objetivo teórico "
              f"{objetivo_teorico(x):>7,.0f} USD; inventario en el "
              f"minuto 20: {x[20]:>7,.0f}")
    return m, x

print(f"referencias: AC {objetivo_teorico(x_ac):,.0f}, TWAP "
      f"{objetivo_teorico(x_tw):,.0f}; inventario AC en el "
      f"minuto 20: {x_ac[20]:,.0f}")
ppo_ruido, x_ruido = entrena_ejecucion(ruido=True, bloques=2)
OUT [04]PPO con la recompensa realizada
referencias: AC 35,385, TWAP 36,584; inventario AC en el minuto 20: 97,150
 30000 pasos: objetivo teórico  35,623 USD; inventario en el minuto 20: 108,522
 60000 pasos: objetivo teórico  36,275 USD; inventario en el minuto 20: 112,630

Con la recompensa realizada, el agente aprende algo en los primeros 30.000 pasos (35.623 USD de objetivo teórico, mejor que TWAP y a 238 de Almgren-Chriss) y lo pierde en los 30.000 siguientes (36.275): su inventario en el minuto 20 sube de 108.522 a 112.630 acciones, más lejos de las 97.150 de la trayectoria óptima. El ruido de cada episodio (decenas de miles de USD) tapa las diferencias entre trayectorias (unos cientos), y cada actualización de PPO empuja en la dirección de los episodios que tuvieron suerte. El resultado depende de dónde se pare el entrenamiento, y con un mercado real no habría forma de saber cuál de los dos quedarse.

Ahora con la recompensa sin el término de ruido, la misma esperanza:

IN [05]python
ppo_exec, x_rl = entrena_ejecucion(ruido=False, bloques=3)
OUT [05]PPO con la recompensa esperada
 30000 pasos: objetivo teórico  35,993 USD; inventario en el minuto 20:  71,420
 60000 pasos: objetivo teórico  35,772 USD; inventario en el minuto 20:  82,671
 90000 pasos: objetivo teórico  35,503 USD; inventario en el minuto 20:  94,107

Sin el término de ruido, el aprendizaje es estable: 35.993, 35.772 y 35.503 USD, cada bloque más cerca de los 35.385 de Almgren-Chriss, y el inventario en el minuto 20 converge hacia el de la trayectoria óptima (94.107 frente a 97.150). La figura de la cabecera dibuja las trayectorias de inventario: el agente reproduce la curva de Almgren-Chriss, que vende deprisa al principio, y se separa de la recta de TWAP. No es casualidad que la mejora venga del diseño de la recompensa y no del algoritmo: en RL aplicado, la recompensa es la mitad del modelo.

Fig. — Objetivo de Almgren-Chriss (coste medio + λ·varianza) en los 1.000 caminos de prueba: las dos referencias y los dos agentes PPO

La evaluación que cuenta es en los 1.000 caminos de prueba. Como las tres estrategias se evalúan en los mismos caminos, la diferencia de costes camino a camino tiene mucho menos ruido que cada coste por separado; su error típico sale de esas diferencias:

IN [06]python
estr = {"TWAP": x_tw, "Almgren-Chriss": x_ac,
        "PPO, recompensa realizada": x_ruido,
        "PPO, recompensa esperada": x_rl}
costes = {k: coste_is(x, xi) for k, x in estr.items()}
tabla = pd.DataFrame({k: resumen(c) for k, c in costes.items()}).T
d = costes["PPO, recompensa esperada"] - costes["Almgren-Chriss"]
print(f"PPO (esperada) − AC, camino a camino: {d.mean():.0f} USD "
      f"de coste medio (error típico {d.std(ddof=1) / 1000**0.5:.0f})")
tabla.round(0)
OUT [06]Agentes frente a las referencias en los caminos de prueba
PPO (esperada) − AC, camino a camino: 332 USD de coste medio (error típico 63)
                             media    desv.  media + λ·var
TWAP                       31866.0  46054.0        38228.0
Almgren-Chriss             32301.0  36915.0        36389.0
PPO, recompensa realizada  32206.0  42584.0        37646.0
PPO, recompensa esperada   32633.0  35551.0        36425.0

En los caminos de prueba, el agente con la recompensa esperada queda en 36.425 USD de media + λ·varianza: 36 por encima de Almgren-Chriss (36.389) y 1.803 por debajo de TWAP (38.228). Camino a camino, paga 332 USD más de coste medio (error típico 63), porque vende algo más deprisa al principio, y lo compensa casi entero con menos varianza (desviación de 35.551 frente a 36.915). El agente de la recompensa realizada, parado a los 60.000 pasos, queda a medio camino: 37.646.

Es el resultado que se espera, y el bueno. El simulador cumple los supuestos de Almgren-Chriss (impacto lineal, precio aritmético, objetivo media-varianza), así que su fórmula es la óptima y un agente solo puede acercarse. Ha necesitado 90.000 pasos (1.500 episodios simulados) y una recompensa rediseñada para llegar a donde la fórmula llega con una línea. Si hubiera quedado por debajo de Almgren-Chriss por más que su error de muestreo, lo correcto sería buscar el error en el entorno, no celebrar.

Market making: el agente contra Avellaneda-Stoikov

El creador de mercado de la lección 2.3.6 cotiza a los dos lados alrededor de un precio de reserva r=s−qγσ2(T−t)r = s - q\gamma\sigma^2(T - t) que se aleja del medio en contra de su inventario qq, con una horquilla total γσ2(T−t)+2γln⁡(1+γ/k)\gamma\sigma^2(T - t) + \frac{2}{\gamma}\ln(1 + \gamma/k). Las órdenes de los demás llegan con intensidad Ae−kδA e^{-k\delta}, que cae con la distancia δ\delta de la cotización al medio. Avellaneda y Stoikov derivan esas fórmulas para un agente con utilidad exponencial de la riqueza final (aversión γ\gamma), con aproximaciones.

El simulador del README (parámetros del artículo: σ = 2, T = 1, dt = 0,005, γ = 0,1, A = 140, k = 1,5) se reescribe vectorizado, para evaluar cualquier regla de sesgo en 1.000 simulaciones con los mismos números aleatorios:

IN [07]python
mm = cfg["market_making"]
SM, SIGM, TM, DT = mm["S0"], mm["sigma"], mm["T"], mm["dt"]
GMM, KMM, AMM = mm["gamma"], mm["k"], mm["A"]
PASOS = int(round(TM / DT))

def horquilla(tl):
    return GMM * SIGM**2 * tl + 2 / GMM * math.log(1 + GMM / KMM)

def simula_mm(sesgo, n=1000, semilla=592):
    """P&L final y |inventario| final de cotizar con el centro en
    S + sesgo(q, t restante) y la horquilla de Avellaneda-Stoikov."""
    rng = np.random.default_rng(semilla)
    z = rng.choice([-1.0, 1.0], (n, PASOS))
    u1, u2 = rng.random((n, PASOS)), rng.random((n, PASOS))
    S, q, caja = np.full(n, SM), np.zeros(n), np.zeros(n)
    for i in range(PASOS):
        tl = TM - i * DT
        centro, h = S + sesgo(q, tl), horquilla(tl) / 2
        pa, pb = centro + h, centro - h
        vende = u1[:, i] < AMM * np.exp(-KMM * (pa - S)) * DT
        compra = u2[:, i] < AMM * np.exp(-KMM * (S - pb)) * DT
        q += compra.astype(float) - vende
        caja += vende * pa - compra * pb
        S = S + SIGM * math.sqrt(DT) * z[:, i]
    return caja + q * S, np.abs(q)

def resumen_mm(sesgo, **kw):
    w, q = simula_mm(sesgo, **kw)
    return {"P&L medio": w.mean(), "desv.": w.std(ddof=1),
            "|q| final": q.mean(),
            "media − γ/2·var": w.mean() - GMM / 2 * w.var(ddof=1)}

def avellaneda(q, tl):
    return -q * GMM * SIGM**2 * tl

refs_mm = {"Avellaneda-Stoikov": avellaneda,
           "simétrica": lambda q, tl: 0 * q}
pd.DataFrame({k: resumen_mm(f) for k, f in refs_mm.items()}).T.round(2)
OUT [07]Simulador vectorizado y las dos referencias del artículo
                    P&L medio  desv.  |q| final  media − γ/2·var
Avellaneda-Stoikov      65.01   6.65       2.19            62.80
simétrica               67.68  13.89       6.97            58.03

Con números aleatorios distintos de los del README, las cifras cambian un poco (65,01 de P&L medio para Avellaneda-Stoikov, frente a 65,12), pero el patrón es el del artículo: la estrategia de inventario gana algo menos en media que la simétrica (65,01 frente a 67,68) con la mitad de desviación (6,65 frente a 13,89) y la tercera parte de inventario final. Con el criterio media − (γ/2)·varianza, la aproximación de segundo orden de la utilidad exponencial con γ = 0,1, gana con claridad: 62,80 frente a 58,03.

El entorno da al agente la misma horquilla de Avellaneda-Stoikov y le deja elegir solo el sesgo: el centro de las cotizaciones es s−βqs - \beta q, con β\beta entre seis valores (Avellaneda-Stoikov equivale a β=0,4(T−t)\beta = 0{,}4(T - t), de 0,4 al principio a 0 al final). La recompensa de cada paso es lo capturado al ejecutarse (la distancia de la cotización al medio) menos la penalización de inventario γ2σ2q2 dt\frac{\gamma}{2}\sigma^2 q^2\,dt, cuya suma esperada es la parte de la varianza del P&L que se debe al inventario. El término q ΔSq\,\Delta S (ganancia o pérdida del inventario por el movimiento del medio) tiene media cero y se quita, como en la ejecución.

IN [08]python
BETAS = np.array([0.0, 0.1, 0.2, 0.3, 0.4, 0.6])

class MMEnv(gym.Env):
    def __init__(self):
        self.observation_space = spaces.Box(-1.0, 1.0, (2,),
                                            np.float32)
        self.action_space = spaces.Discrete(len(BETAS))

    def _obs(self):
        return np.array([np.clip(self.q / 10, -1, 1), self.i / PASOS],
                        np.float32)

    def reset(self, seed=None, options=None):
        super().reset(seed=seed)
        self.S, self.q, self.i = SM, 0, 0
        return self._obs(), {}

    def step(self, a):
        tl = TM - self.i * DT
        centro = self.S - BETAS[int(a)] * self.q
        h = horquilla(tl) / 2
        pa, pb = centro + h, centro - h
        u = self.np_random.random(3)
        captura = 0.0
        if u[0] < AMM * math.exp(-KMM * (pa - self.S)) * DT:
            self.q -= 1
            captura += pa - self.S
        if u[1] < AMM * math.exp(-KMM * (self.S - pb)) * DT:
            self.q += 1
            captura += self.S - pb
        self.S += SIGM * math.sqrt(DT) * (1.0 if u[2] < 0.5 else -1.0)
        self.i += 1
        r = captura - GMM / 2 * SIGM**2 * self.q**2 * DT
        return self._obs(), r, self.i == PASOS, False, {}

check_env(MMEnv(), skip_render_check=True)
vec = make_vec_env(MMEnv, n_envs=8, seed=0)
ppo_mm = PPO("MlpPolicy", vec, gamma=0.99, n_steps=400,
             batch_size=400, learning_rate=3e-4,
             policy_kwargs={"net_arch": [32, 32]},
             seed=0, device="cpu", verbose=0)
ppo_mm.learn(80_000)

def sesgo_agente(q, tl):
    obs = np.column_stack([np.clip(q / 10, -1, 1),
                           np.full(len(q), (TM - tl) / TM)])
    a, _ = ppo_mm.predict(obs.astype(np.float32), deterministic=True)
    return -BETAS[a] * q

rejilla = [(q, t) for t in (0.0, 0.5, 0.9) for q in (-6, -2, 2, 6)]
obs = np.array([[q / 10, t] for q, t in rejilla], np.float32)
print("β elegido:", BETAS[ppo_mm.predict(obs, deterministic=True)[0]])
OUT [08]MMEnv: el agente elige el sesgo de inventario
β elegido: [0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1]

El agente, tras 80.000 pasos (400 sesiones simuladas), ha aprendido una regla más simple que la del artículo: β = 0,1 en los doce puntos de la rejilla, con cualquier inventario y en cualquier momento de la sesión. No reduce el sesgo hacia el cierre, como hace Avellaneda-Stoikov, y empieza con un sesgo cuatro veces menor.

IN [09]python
# búsqueda directa: β constante, elegido con otras simulaciones
busqueda = {b: resumen_mm(lambda q, tl, b=b: -b * q, semilla=1592)
            for b in BETAS}
mejor = max(busqueda, key=lambda b: busqueda[b]["media − γ/2·var"])
print("β elegido por búsqueda directa (semilla 1592):", mejor)
todas = dict(refs_mm)
todas["agente PPO"] = sesgo_agente
todas[f"búsqueda directa, β = {mejor}"] = (
    lambda q, tl: -mejor * q)
pd.DataFrame({k: resumen_mm(f) for k, f in todas.items()}).T.round(2)
OUT [09]Agente, referencias y búsqueda directa del parámetro
β elegido por búsqueda directa (semilla 1592): 0.1
                           P&L medio  desv.  |q| final  media − γ/2·var
Avellaneda-Stoikov             65.01   6.65       2.19            62.80
simétrica                      67.68  13.89       6.97            58.03
agente PPO                     66.58   6.99       1.16            64.13
búsqueda directa, β = 0.1      66.58   6.99       1.16            64.13
Fig. — Market making en 1.000 simulaciones comunes del modelo de Avellaneda-Stoikov: P&L medio frente a su desviación de la fórmula del artículo, la cotización simétrica y el sesgo constante que eligen PPO y la búsqueda directa; entre paréntesis, media − (γ/2)·varianza

El agente gana a Avellaneda-Stoikov con el criterio de la tabla: 64,13 frente a 62,80, con más P&L medio (66,58 frente a 65,01), una desviación parecida (6,99 frente a 6,65) y la mitad de inventario final (1,16 frente a 2,19). La figura sitúa las cuatro estrategias en el plano de media y desviación.

¿Ha descubierto el RL algo que el artículo no sabía? No. Avellaneda-Stoikov es óptimo para su objetivo (utilidad exponencial de la riqueza final, con una aproximación de las llegadas para inventarios pequeños y sesiones largas), no para la media − varianza de este simulador con su paso de tiempo. El agente optimiza otro criterio y gana con ese criterio. Y la búsqueda directa (probar los seis valores de β constante en otras 1.000 simulaciones y quedarse con el mejor) encuentra exactamente lo mismo, β = 0,1, en un segundo y sin redes. Cuando la política óptima cabe en un parámetro, un agente de RL es la forma más cara de encontrarlo. Lo que el RL podría aportar está en lo que este simulador no tiene: llegadas que dependen del flujo informado y de la selección adversa (lección 2.3.6), colas en el libro, varios activos correlacionados. Ahí no hay fórmula cerrada, y también es donde el simulador es más difícil de creer.

Cobertura: deep hedging ligero bajo Heston

El último problema es el de la lección 3.6.2: se vende una call ATM a 21 sesiones sobre 100 acciones, cobrando la prima de Black-Scholes con σ = 20 % (230,30 USD), y se cubre al cierre de cada sesión con el subyacente, pagando un 0,05 % del nominal negociado. El subyacente no sigue Black-Scholes sino Heston: volatilidad estocástica con correlación negativa con el precio (ρ = −0,7) y volatilidad de la volatilidad 0,5. La delta de Black-Scholes es la cobertura óptima sin costes y con volatilidad constante; aquí no se cumple ninguna de las dos cosas.

Deep hedging (Buehler et al., 2019) plantea la cobertura como una política: una red decide la posición de cada día a partir de lo observable, y se entrena maximizando directamente el criterio de riesgo del P&L final sobre muchos caminos simulados. Como el P&L es una función diferenciable de las posiciones, el gradiente se calcula con retropropagación a través de toda la simulación, sin estimar ventajas como PPO: es una búsqueda directa de la política con un simulador diferenciable, el pariente eficiente del RL cuando el simulador lo permite. La versión ligera de la lección:

  • Entradas de la red, observables al cierre de cada día: log(S/K), el tiempo restante, la posición actual y la delta de Black-Scholes con σ = 20 %. La varianza de Heston no se observa.
  • Salida: una corrección de la delta, posicioˊn=clip(ΔBS+0,5tanh⁡(fθ),0,1)\text{posición} = \text{clip}(\Delta_{BS} + 0{,}5\tanh(f_\theta), 0, 1). La red parte de la delta y aprende cuánto desviarse.
  • Criterio: maximizar media − 0,01·varianza del P&L final en USD, la última columna de la tabla del README.
  • Entrenamiento: 20.000 caminos de Heston simulados en la lección con otra semilla (mismos parámetros que el JSON) y minilotes de 4.000. Evaluación: los 2.000 caminos de prueba del Parquet, que la red no ve.
IN [10]python
co = cfg["cobertura"]
K, ACC, C_TR = co["S0"], co["acciones"], co["coste"]
ND, SIG_BS, PRIMA = co["T_dias"], co["sigma_impl"], co["prima_usd"]

cob = pd.read_parquet("rl5_caminos_cobertura.parquet")
S_prueba = torch.tensor(cob.pivot(index="camino", columns="dia",
                                  values="precio").to_numpy())

def heston(m, rng):
    """Caminos diarios de Heston (Euler con truncamiento)."""
    dt = 1 / co["dias_ano"] / co["subpasos"]
    S, v = np.full(m, co["S0"]), np.full(m, co["v0"])
    sal = [S.copy()]
    rho = co["rho"]
    for _ in range(ND):
        for _ in range(co["subpasos"]):
            z1 = rng.standard_normal(m)
            z2 = (rho * z1
                  + math.sqrt(1 - rho**2) * rng.standard_normal(m))
            vp = np.maximum(v, 0)
            S = S * np.exp(-vp / 2 * dt + np.sqrt(vp * dt) * z1)
            v = (v + co["kappa"] * (co["theta"] - vp) * dt
                 + co["xi"] * np.sqrt(vp * dt) * z2)
        sal.append(S.copy())
    return torch.tensor(np.array(sal).T)

S_ent = heston(20_000, np.random.default_rng(11))

def delta_bs(S, tau):
    d1 = ((torch.log(S / K) + SIG_BS**2 * tau / 2)
          / (SIG_BS * math.sqrt(tau)))
    return 0.5 * (1 + torch.erf(d1 / math.sqrt(2)))

def pnl_cobertura(S, politica):
    """P&L final (USD) de vender la call y cubrir cada cierre con
    politica(S_d, tau, pos, delta) -> posición nueva por acción."""
    m = S.shape[0]
    pos = torch.zeros(m, dtype=S.dtype)
    caja = torch.full((m,), PRIMA, dtype=S.dtype)
    for d in range(ND):
        tau = (ND - d) / co["dias_ano"]
        nueva = politica(S[:, d], tau, pos, delta_bs(S[:, d], tau))
        dq = (nueva - pos) * ACC
        caja = caja - dq * S[:, d] - C_TR * torch.abs(dq) * S[:, d]
        pos = nueva
    final = pos * ACC * S[:, -1]
    caja = caja + final - C_TR * torch.abs(final)
    return caja - torch.clamp(S[:, -1] - K, min=0) * ACC

def criterio(p):
    return float(p.mean() - 0.01 * p.var())

def banda(b):
    return lambda S, tau, pos, de: torch.where(
        torch.abs(de - pos) > b, de, pos)

refs_co = {"sin cubrir": lambda S, tau, pos, de: 0 * de,
           "delta BS diaria": lambda S, tau, pos, de: de,
           "delta BS con banda 0,05": banda(0.05)}
filas = {}
for k, f in refs_co.items():
    p = pnl_cobertura(S_prueba, f)
    filas[k] = {"media": float(p.mean()), "desv.": float(p.std()),
                "cuantil 5 %": float(torch.quantile(p, 0.05)),
                "media − 0,01·var": criterio(p)}
pd.DataFrame(filas).T.round(2)
OUT [10]Caminos de prueba y de entrenamiento; la delta BS reproduce el README
                         media   desv.  cuantil 5 %  media − 0,01·var
sin cubrir               -1.91  296.71      -604.05           -882.28
delta BS diaria          -5.37   57.11       -98.08            -37.99
delta BS con banda 0,05  -4.64   58.05       -97.94            -38.34

Las cifras son las del README: la delta diaria con costes da −37,99 de criterio; la banda de 0,05, que opera menos, no lo mejora (−38,34). Sin cubrir, el criterio es −882: la cobertura reduce la desviación de 297 a 57 USD.

IN [11]python
torch.manual_seed(0)
red = torch.nn.Sequential(
    torch.nn.Linear(4, 32), torch.nn.ReLU(),
    torch.nn.Linear(32, 32), torch.nn.ReLU(),
    torch.nn.Linear(32, 1)).double()

def politica_red(S, tau, pos, de):
    entrada = torch.stack([torch.log(S / K) * 5,
                           torch.full_like(S, tau * 12), pos, de], 1)
    return torch.clamp(de + 0.5 * torch.tanh(red(entrada)[:, 0]),
                       0, 1)

delta_ent = pnl_cobertura(S_ent, refs_co["delta BS diaria"])
print(f"delta BS en los caminos de entrenamiento: "
      f"{criterio(delta_ent):.2f}")
opt = torch.optim.Adam(red.parameters(), lr=1e-3)
gen = torch.Generator().manual_seed(0)
for it in range(1, 201):
    lote = torch.randperm(len(S_ent), generator=gen)[:4000]
    p = pnl_cobertura(S_ent[lote], politica_red)
    perdida = -(p.mean() - 0.01 * p.var())
    opt.zero_grad()
    perdida.backward()
    opt.step()
    if it % 50 == 0:
        with torch.no_grad():
            ent = criterio(pnl_cobertura(S_ent, politica_red))
            pru = criterio(pnl_cobertura(S_prueba, politica_red))
        print(f"iteración {it:>3}: criterio entrenamiento {ent:7.2f}"
              f", prueba {pru:7.2f}")
OUT [11]Deep hedging: la red corrige la delta
delta BS en los caminos de entrenamiento: -42.26
iteración  50: criterio entrenamiento  -40.30, prueba  -36.63
iteración 100: criterio entrenamiento  -39.79, prueba  -36.12
iteración 150: criterio entrenamiento  -39.57, prueba  -35.94
iteración 200: criterio entrenamiento  -39.45, prueba  -35.84

Los dos criterios mejoran a la vez. En los caminos de entrenamiento, de −42,26 con la delta a −39,45 con la red (2,81 USD); en los de prueba, de −37,99 a −35,84. Los niveles no son comparables entre conjuntos (son caminos distintos y los de entrenamiento salen más dispersos), pero las mejoras sí, y la de prueba es algo menor que la de entrenamiento, como debe ser. Con 20.000 caminos para unos 1.200 parámetros, el sobreajuste a la muestra es pequeño; con 2.000 caminos sería otra historia. En producción se entrenaría más (aquí, 200 pasos de Adam para que la lección corra en segundos) y con más caminos.

IN [12]python
with torch.no_grad():
    p_red = pnl_cobertura(S_prueba, politica_red)
    p_bs = pnl_cobertura(S_prueba, refs_co["delta BS diaria"])
    # costes de transacción pagados por cada estrategia
    sin_coste = {}
    for nombre, f in (("red", politica_red),
                      ("delta", refs_co["delta BS diaria"])):
        C_TR, guarda = 0.0, C_TR
        sin_coste[nombre] = pnl_cobertura(S_prueba, f)
        C_TR = guarda
for nombre, p in (("delta BS diaria", p_bs), ("deep hedging", p_red)):
    clave = "delta" if nombre.startswith("delta") else "red"
    costes = float((sin_coste[clave] - p).mean())
    print(f"{nombre:<16} media {float(p.mean()):6.2f}  desv. "
          f"{float(p.std()):5.2f}  costes {costes:5.2f}  "
          f"criterio {criterio(p):7.2f}")
rng = np.random.default_rng(0)
mejoras = []
for _ in range(1000):
    i = torch.from_numpy(rng.integers(0, len(p_red), len(p_red)))
    mejoras.append(criterio(p_red[i]) - criterio(p_bs[i]))
print(f"mejora del criterio: {criterio(p_red) - criterio(p_bs):.2f}"
      f" USD (error típico bootstrap {np.std(mejoras):.2f})")
OUT [12]La mejora en los caminos de prueba, con su error típico
delta BS diaria  media  -5.37  desv. 57.11  costes 12.23  criterio  -37.99
deep hedging     media  -4.66  desv. 55.84  costes 11.23  criterio  -35.84
mejora del criterio: 2.14 USD (error típico bootstrap 0.65)
Fig. — Criterio media − 0,01·varianza del P&L de cubrir una call vendida bajo Heston en los 2.000 caminos de prueba: el deep hedging frente a la delta de Black-Scholes con y sin bandas (menos negativo es mejor)

La red mejora el criterio de la delta en 2,14 USD, con un error típico bootstrap de 0,65 (más de tres errores típicos), y lo hace por las dos vías que la teoría anticipa: opera algo menos (11,23 USD de costes de transacción frente a 12,23, un 8 % menos) y cubre algo mejor bajo Heston (desviación de 55,84 frente a 57,11). La figura compara el criterio de las coberturas. Es una mejora pequeña, unos 2 USD sobre una prima de 230, pero sistemática: en un libro de opciones que se cubre cada día, es dinero. Y llega donde la banda fija de no negociar, la regla manual para operar menos, empeoraba el resultado (−38,34).

Qué aporta el agente y qué no

ProblemaReferenciaAgente en los caminos de pruebaPor qué
EjecuciónAlmgren-Chriss+36 USD de objetivo: igualel simulador cumple sus supuestos
Market makingAvellaneda-Stoikov64,13 frente a 62,80, igual que la búsqueda directaotro criterio; un parámetro basta
Coberturadelta BS con costes+2,14 USD de criterio (error típico 0,65)Heston y costes rompen sus supuestos

El patrón de los tres casos es el que anticipaba la introducción. Donde el simulador cumple los supuestos de la solución analítica (ejecución), el agente la iguala, y más caro. Donde el agente parece ganar a la referencia (market making), conviene preguntar con qué criterio: si el negocio quiere media − varianza del P&L de la sesión, la pregunta útil es por qué usar una fórmula derivada para otro objetivo, y una búsqueda directa responde más barato que un agente. Donde la referencia hace supuestos que el simulador no cumple (cobertura con costes bajo Heston), un modelo aprendido mejora, poco y de forma medible. En los tres, lo que hace creíble el resultado es lo mismo: una referencia conocida, caminos de prueba comunes que el agente no ha visto y el error típico de la diferencia.

Ejercicios

  1. Repite la búsqueda directa del sesgo de market making con un criterio más averso, media − 0,2·varianza (en vez de γ/2 = 0,05). ¿Qué β elige y cómo queda Avellaneda-Stoikov con ese criterio?
Solución
IN [13]python
def crit_02(sesgo, semilla):
    w, _ = simula_mm(sesgo, semilla=semilla)
    return w.mean() - 0.2 * w.var(ddof=1)

b02 = {b: crit_02(lambda q, tl, b=b: -b * q, 1592) for b in BETAS}
mejor02 = max(b02, key=b02.get)
print("β elegido:", mejor02)
prueba = crit_02(lambda q, tl: -mejor02 * q, 592)
print(f"en la prueba: búsqueda {prueba:.2f}, "
      f"Avellaneda-Stoikov {crit_02(avellaneda, 592):.2f}")
OUT [13]Búsqueda directa con más aversión
β elegido: 0.1
en la prueba: búsqueda 56.80, Avellaneda-Stoikov 56.17

Con un criterio cuatro veces más averso, la búsqueda vuelve a elegir β = 0,1 (la rejilla es gruesa: 0,1 y 0,2 son los únicos candidatos razonables) y sigue por delante de Avellaneda-Stoikov en la prueba (56,80 frente a 56,17), pero la distancia se reduce de 1,33 a 0,63. Cuanto más pesa la varianza, más se acerca la fórmula, que reduce el inventario con fuerza al principio de la sesión. Con una rejilla más fina o con β dependiente del tiempo el resultado podría cambiar; es justo el tipo de pregunta que una búsqueda directa responde en segundos.

  1. Evalúa la red de deep hedging ya entrenada en caminos de Heston con una volatilidad de la volatilidad de 0,8 en vez de 0,5 (2.000 caminos nuevos, semilla 12), junto a la delta BS. ¿Se mantiene la mejora fuera del simulador de entrenamiento?
Solución
IN [14]python
co_orig = co["xi"]
co["xi"] = 0.8
S_otro = heston(2_000, np.random.default_rng(12))
co["xi"] = co_orig
with torch.no_grad():
    for nombre, f in (("delta BS diaria", refs_co["delta BS diaria"]),
                      ("deep hedging", politica_red)):
        p = pnl_cobertura(S_otro, f)
        print(f"{nombre:<16} desv. {float(p.std()):6.2f}  "
              f"criterio {criterio(p):8.2f}")
OUT [14]La red en un Heston distinto
delta BS diaria  desv.  77.14  criterio   -59.02
deep hedging     desv.  73.55  criterio   -52.53

Con ξ = 0,8 las dos coberturas empeoran (desviación de 77,14 y 73,55 USD) y la ventaja de la red crece: 6,49 USD de criterio (−52,53 frente a −59,02). La corrección que aprendió, operar algo menos y ajustar la cobertura a la correlación negativa entre precio y volatilidad, va en la buena dirección también con más volatilidad de la volatilidad. No es una garantía: cambiar un parámetro dentro de la misma familia de modelos mide la robustez a la calibración, no al error de modelo (saltos, regímenes, costes que no son proporcionales).

  1. En el entorno de ejecución, ¿qué pasaría si se añadiera a la observación el movimiento del precio desde el inicio y se entrenara con la recompensa realizada? ¿Podría el agente mejorar a Almgren-Chriss?
Solución

En este modelo, no. El precio sigue un paseo aleatorio aritmético con innovaciones independientes y el impacto no depende del precio, así que el movimiento pasado no dice nada del futuro: la trayectoria óptima para media − λ·varianza es determinista (Almgren y Chriss la llaman estática, y para este criterio y este modelo las estrategias que reaccionan al precio solo mejoran de forma marginal y con otro planteamiento del objetivo). Con la recompensa realizada, el agente vería correlaciones espurias entre el precio observado y la recompensa de cada episodio y aprendería a reaccionar a ellas: en los caminos de entrenamiento podría parecer mejor; en los de prueba, sería igual o peor que Almgren-Chriss, con más varianza. Es el sobreajuste de la lección 5.9.3 en pequeño. Si el precio tuviera reversión a la media o el impacto dependiera del estado del libro, la observación sí aportaría y un agente podría ganar a la fórmula estática, que es justo lo que exploran trabajos como Nevmyvaka, Feng y Kearns (2006) con datos de libro reales.

Respuesta plegada en cada pregunta

Autoevaluación

  1. En el entorno de ejecución, la recompensa de cada paso incluye el término x_k·σ·√τ·ξ_k, la ganancia o pérdida por el movimiento del precio con el inventario que queda. ¿Por qué se puede quitar al entrenar sin cambiar la política óptima?

    • APorque es pequeño frente a los costes de impacto
    • BPorque su esperanza es cero dada cualquier decisión (ξ_k es independiente de x_k), y el riesgo que representa ya está en la penalización λ·σ²·τ·x_k²; quitarlo reduce la varianza de la recompensa, no cambia su esperanza
    • CPorque Almgren-Chriss supone que el precio no se mueve
    • DPorque PPO no admite recompensas negativas
    Ver respuesta

    Respuesta B. Porque su esperanza es cero dada cualquier decisión (ξ_k es independiente de x_k), y el riesgo que representa ya está en la penalización λ·σ²·τ·x_k²; quitarlo reduce la varianza de la recompensa, no cambia su esperanza

    Es una variable de control: el objetivo esperado es el mismo con o sin él. Con él, la recompensa de un episodio tiene una desviación de decenas de miles de USD frente a diferencias de unos cientos entre estrategias, y PPO no distingue bien.

  2. El agente de ejecución entrenado con la recompensa sin ruido queda en los caminos de prueba a 36 USD de Almgren-Chriss en media + λ·varianza, y 1.803 por delante de TWAP. ¿Cómo se lee?

    • AQue ha aprendido una trayectoria casi igual a la óptima: en este modelo (impacto lineal, precio aritmético), la solución analítica es exacta, y lo máximo que puede hacer un agente es igualarla
    • BQue el agente ha superado a Almgren-Chriss
    • CQue TWAP es la mejor estrategia
    • DQue hay una fuga en el entorno
    Ver respuesta

    Respuesta A. Que ha aprendido una trayectoria casi igual a la óptima: en este modelo (impacto lineal, precio aritmético), la solución analítica es exacta, y lo máximo que puede hacer un agente es igualarla

    Cuando el simulador cumple los supuestos del modelo analítico, el RL solo puede reproducirlo, y más caro. Si el agente ganara por más que el error de muestreo, habría un error en el entorno (lo advierte el README de los datos).

  3. En el market making, un sesgo de inventario constante (β = 0,1) da más media − (γ/2)·varianza que la fórmula de Avellaneda-Stoikov. ¿Qué se concluye?

    • AQue Avellaneda-Stoikov está mal
    • BQue el agente es más listo que el modelo
    • CQue la fórmula es óptima para su propio objetivo (utilidad exponencial con aproximaciones) y el agente optimiza otro (media − varianza del P&L con este simulador): la comparación depende del criterio, y una búsqueda directa del parámetro da lo mismo que PPO
    • DQue el market making no tiene riesgo de inventario
    Ver respuesta

    Respuesta C. Que la fórmula es óptima para su propio objetivo (utilidad exponencial con aproximaciones) y el agente optimiza otro (media − varianza del P&L con este simulador): la comparación depende del criterio, y una búsqueda directa del parámetro da lo mismo que PPO

    Ganar a una referencia con un criterio que no es el suyo no es una victoria del RL. Lo útil es la pregunta que obliga a hacer: qué objetivo tiene el negocio y si la referencia lo optimiza.

  4. El deep hedging de la lección mejora la media − 0,01·varianza del P&L de la delta de Black-Scholes con costes en los 2.000 caminos de prueba. ¿Por qué es plausible que un modelo aprendido gane aquí?

    • APorque la red ve la varianza verdadera de Heston
    • BPorque los caminos de prueba se usaron para entrenar
    • CPorque las redes neuronales siempre ganan a las fórmulas
    • DPorque la delta de Black-Scholes es óptima solo sin costes y con volatilidad constante; bajo Heston con correlación negativa y con costes, operar algo menos y ajustar la cobertura a la asimetría reduce la varianza y los costes
    Ver respuesta

    Respuesta D. Porque la delta de Black-Scholes es óptima solo sin costes y con volatilidad constante; bajo Heston con correlación negativa y con costes, operar algo menos y ajustar la cobertura a la asimetría reduce la varianza y los costes

    El agente gana donde la referencia hace supuestos que el simulador no cumple. Se entrena con caminos propios y se evalúa en los de prueba, que nunca ha visto; la mejora se da con su error típico.

  5. ¿Cuál es el principal riesgo de llevar a producción cualquiera de estos tres agentes?

    • AQue PPO no es determinista
    • BQue los caminos de prueba son pocos
    • CQue el agente aprende el simulador: si el impacto real no es lineal, o la volatilidad no sigue Heston, lo aprendido puede ser peor que la fórmula, y en los caminos de prueba del mismo simulador no se ve
    • DQue las redes son demasiado pequeñas
    Ver respuesta

    Respuesta C. Que el agente aprende el simulador: si el impacto real no es lineal, o la volatilidad no sigue Heston, lo aprendido puede ser peor que la fórmula, y en los caminos de prueba del mismo simulador no se ve

    Los caminos de prueba miden el sobreajuste a la semilla de entrenamiento, no el error de modelo. Hace falta probar el agente en simuladores con supuestos distintos (impacto en raíz cuadrada de 4.3.1, otros parámetros de Heston) y, con datos reales, con un backtest de ejecución.

Ideas clave

  • Los tres problemas tienen un simulador, un objetivo claro (media − λ·varianza) y una referencia analítica: es donde el RL en trading tiene sentido y donde se puede medir.
  • Ejecución: con la recompensa realizada, PPO aprende y desaprende (35.623 y 36.275 USD de objetivo teórico a los 30.000 y 60.000 pasos); quitando el ruido de media cero, queda a 36 USD de Almgren-Chriss en los caminos de prueba (36.425 frente a 36.389; TWAP, 38.228).
  • Cuando el simulador cumple los supuestos de la solución analítica, el agente como mucho la iguala. Si gana, hay un error en el entorno.
  • Market making: el agente elige un sesgo constante β = 0,1 que da 64,13 de media − (γ/2)·varianza frente a 62,80 de Avellaneda-Stoikov, porque optimiza otro criterio; una búsqueda directa del parámetro encuentra lo mismo en un segundo.
  • Cobertura: un deep hedging ligero (red de 32 × 32, entrenado con 20.000 caminos Heston propios) mejora en 2,14 USD (error típico 0,65) el criterio de la delta BS con costes en los 2.000 caminos de prueba, con menos desviación y menos costes. Gana porque la delta BS es óptima con supuestos que Heston y los costes no cumplen.
  • El riesgo de los tres es el mismo: el agente aprende el simulador. Los caminos de prueba miden el sobreajuste a la semilla, no el error de modelo.

Recursos