---
title: "5.9.2 RL para ejecución, market making y hedging"
description: "Agentes de RL frente a Almgren-Chriss, Avellaneda-Stoikov y la delta de Black-Scholes, medidos en los mismos caminos de prueba fijos."
url: "https://pythonparatrading.com/curso-python-trading/nivel-5/rl-para-ejecucion-market-making-y-hedging/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-5/rl-para-ejecucion-market-making-y-hedging.png"
---

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

- Curso: [Nivel 5: Machine learning y deep learning (Keras/PyTorch)](https://pythonparatrading.com/curso-python-trading/nivel-5/)
- Módulo: 5.9 · Aprendizaje por refuerzo
- Duración: 75 min
- Requisitos: [5.9.1 · MDP, Q-learning, PPO y entornos Gymnasium](https://pythonparatrading.com/curso-python-trading/nivel-5/mdp-q-learning-ppo-y-entornos-gymnasium/), [4.3.1 · Implementation shortfall, VWAP, TWAP y Almgren-Chriss](https://pythonparatrading.com/curso-python-trading/nivel-4/implementation-shortfall-vwap-twap-y-almgren-chriss/), [2.3.6 · Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)](https://pythonparatrading.com/curso-python-trading/nivel-2/market-making-avellaneda-stoikov/), [3.6.2 · Delta hedging, gamma scalping y dispersión](https://pythonparatrading.com/curso-python-trading/nivel-3/delta-hedging-gamma-scalping-y-dispersion/)

## Al terminar esta lección

- Diseñar la recompensa de un problema de ejecución para que su suma sea el objetivo media-varianza de Almgren-Chriss, y quitarle el ruido de media cero sin cambiar la solución.
- Entrenar PPO para liquidar una posición y evaluarlo en los 1.000 caminos de prueba frente a TWAP y Almgren-Chriss, con el error de la diferencia.
- Plantear el market making de Avellaneda-Stoikov como entorno, comparar el agente con la referencia y con una búsqueda directa del parámetro, y entender qué criterio se está comparando.
- Programar un deep hedging ligero con PyTorch (diferenciando a través de la simulación) y medir si mejora a la delta de Black-Scholes con costes bajo Heston.
- Distinguir en cada caso si el agente aporta algo que el modelo analítico no tenía.

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

| Problema | Acción | Objetivo | Referencia analítica | Lección |
|---|---|---|---|---|
| Ejecución óptima | cuánto vender en cada minuto | coste + λ·varianza | Almgren-Chriss | 4.3.1 |
| Market making | dónde poner las cotizaciones | P&L − riesgo de inventario | Avellaneda-Stoikov | 2.3.6 |
| Cobertura dinámica | cuánto subyacente tener | P&L − λ·varianza | delta de Black-Scholes | 3.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:

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

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

`rl5_*` son datos sintéticos: parámetros de modelos publicados (Almgren y Chriss, 2000; Avellaneda y Stoikov, 2008;
Heston) y caminos simulados con semilla fija. Sirven para medir métodos. Un agente que gane aquí ha aprendido este
simulador, no un mercado.

</aside>

## 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 $S_k = S_{k-1} + \sigma\sqrt{\tau}\,\xi_k - \tau\gamma v_k$ (impacto permanente $\gamma$) y la
venta del paso $k$, $n_k$ acciones a ritmo $v_k = n_k/\tau$, se ejecuta a $S_{k-1} - \varepsilon - \eta v_k$ (horquilla
e impacto temporal). El coste es el *implementation shortfall* $C = X S_0 - \sum_k n_k \cdot \text{precio}_k$ y el
objetivo, minimizar $\mathbb{E}[C] + \lambda \operatorname{Var}[C]$. Almgren y Chriss dan la trayectoria óptima de
inventario en forma cerrada, $x_k = X \sinh(\kappa(T - t_k)) / \sinh(\kappa T)$, que está en el JSON.

```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%}")
```
```output
(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):

```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)
```
```output
                  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 + \lambda V)$.
Escribiendo el precio de cada venta como $S_0$ más los movimientos anteriores, el coste se reparte por pasos:

$$
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 $x_k$ el inventario que queda tras la venta $k$. 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 $\sigma^2\tau\sum_k x_k^2$, así que la recompensa
del paso $k$ es

$$
r_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: $\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 $\lambda\sigma^2\tau x_k^2$ ya representa el riesgo.
El entorno admite las dos versiones.

```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")
```
```output
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 ($\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:

```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)
```
```output
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:

```python
ppo_exec, x_rl = entrena_ejecucion(ruido=False, bloques=3)
```
```output
 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.* Barras horizontales en miles de USD, el eje empieza en 34: TWAP 38.228, PPO con la recompensa realizada 37.646, PPO con la recompensa esperada 36.425 (destacado) y Almgren-Chriss 36.389.

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:

```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)
```
```output
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\gamma\sigma^2(T - t)$ que se aleja del medio en contra de su inventario $q$, con una horquilla total
$\gamma\sigma^2(T - t) + \frac{2}{\gamma}\ln(1 + \gamma/k)$. Las órdenes de los demás llegan con intensidad
$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:

```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)
```
```output
                    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 - \beta q$, con $\beta$ entre seis valores (Avellaneda-Stoikov equivale a $\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 $\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\,\Delta S$ (ganancia o pérdida del inventario
por el movimiento del medio) tiene media cero y se quita, como en la ejecución.

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

```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)
```
```output
β 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.* Tres puntos. Avellaneda-Stoikov: desviación 6,65 y media 65,01 (media − γ/2·varianza 62,80). Simétrica: 13,89 y 67,68 (58,03). Agente PPO e idéntica búsqueda directa con β = 0,1 (destacado): 6,99 y 66,58 (64,13).

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, $\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.

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

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

```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})")
```
```output
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).* Barras horizontales negativas: delta BS sin costes −25,30 (cota sin costes, del README), deep hedging −35,84 (destacado), delta BS diaria −37,99, delta con banda 0,05 −38,34 y con banda 0,10 −43,24 (README).

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

| Problema | Referencia | Agente en los caminos de prueba | Por qué |
|---|---|---|---|
| Ejecución | Almgren-Chriss | +36 USD de objetivo: igual | el simulador cumple sus supuestos |
| Market making | Avellaneda-Stoikov | 64,13 frente a 62,80, igual que la búsqueda directa | otro criterio; un parámetro basta |
| Cobertura | delta 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.

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

Los caminos de prueba protegen de un tipo de sobreajuste, el de la semilla de entrenamiento, y de ninguno más. Un
agente entrenado en un simulador aprende el simulador: si el impacto real sigue la raíz cuadrada (lección 4.3.1) y no
la recta, si las llegadas de órdenes dependen del flujo informado, si la volatilidad no es Heston, lo aprendido puede
ser peor que la fórmula, y los caminos de prueba del mismo simulador no lo dirán. Antes de usarlo, el agente se prueba
en simuladores con supuestos distintos y, con datos reales, en un backtest de ejecución o de cobertura.

</aside>

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

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

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

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

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

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

   - A. Porque es pequeño frente a los costes de impacto
   - 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
   - C. Porque Almgren-Chriss supone que el precio no se mueve
   - D. Porque PPO no admite recompensas negativas

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

   - 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
   - B. Que el agente ha superado a Almgren-Chriss
   - C. Que TWAP es la mejor estrategia
   - D. Que hay una fuga en el entorno

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

   - A. Que Avellaneda-Stoikov está mal
   - B. Que el agente es más listo que el modelo
   - 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
   - D. Que el market making no tiene riesgo de inventario

   *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í?

   - A. Porque la red ve la varianza verdadera de Heston
   - B. Porque los caminos de prueba se usaron para entrenar
   - C. Porque las redes neuronales siempre ganan a las fórmulas
   - 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

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

   - A. Que PPO no es determinista
   - B. Que los caminos de prueba son pocos
   - 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
   - D. Que las redes son demasiado pequeñas

   *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

### Research

- [Modelos Cuantitativos y Ejecución Algorítmica](https://pythonparatrading.com/modelos-cuantitativos-ejecucion-algoritmica/) · Research-0009 · 11 min

### Documentación

- [Almgren, R. y Chriss, N. (2000/2001): Optimal execution of portfolio transactions. Journal of Risk 3(2), 5-39](https://doi.org/10.21314/JOR.2001.041) · doi.org
- [Avellaneda, M. y Stoikov, S. (2008): High-frequency trading in a limit order book. Quantitative Finance 8(3)](https://doi.org/10.1080/14697680701381228) · doi.org
- [Buehler, H., Gonon, L., Teichmann, J. y Wood, B. (2019): Deep hedging. Quantitative Finance 19(8). arXiv:1802.03042](https://arxiv.org/abs/1802.03042) · arxiv.org
- [Nevmyvaka, Y., Feng, Y. y Kearns, M. (2006): Reinforcement learning for optimized trade execution. ICML](https://doi.org/10.1145/1143844.1143929) · doi.org
- [Stable-Baselines3: PPO](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) · stable-baselines3.readthedocs.io

### Datos

- [Datos del nivel 5: parámetros y referencias de los entornos de RL (JSON)](https://pythonparatrading.com/curso/datos/nivel-5/rl5_entornos.json) · Descarga · rl5_entornos.json
- [Datos del nivel 5: caminos de prueba de la cobertura con Heston (Parquet)](https://pythonparatrading.com/curso/datos/nivel-5/rl5_caminos_cobertura.parquet) · Descarga · rl5_caminos_cobertura.parquet

Lección anterior: [5.9.1 · MDP, Q-learning, PPO y entornos Gymnasium](https://pythonparatrading.com/curso-python-trading/nivel-5/mdp-q-learning-ppo-y-entornos-gymnasium/)

Lección siguiente: [5.9.3 · Por qué el RL sobreajusta en trading direccional](https://pythonparatrading.com/curso-python-trading/nivel-5/por-que-el-rl-sobreajusta-en-trading-direccional/)
