---
title: "4.2.1 Riesgo por operación, stops y riesgo de ruina"
description: "Tamaño por riesgo con contratos enteros, stops fijos, ATR, trailing y temporales medidos en 24 futuros, y riesgo de ruina por fórmula y simulación."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/riesgo-por-operacion-stops-y-riesgo-de-ruina/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/riesgo-por-operacion-stops-y-riesgo-de-ruina.png"
---

# 4.2.1 · Riesgo por operación, stops y riesgo de ruina

> Antes de entrar en una operación se decide cuánto se está dispuesto a perder si sale mal: el riesgo por operación, R. De él salen el tamaño de la posición y el sentido del stop. Esta lección mide sobre un sistema de tendencia en 24 futuros qué hace cada tipo de stop (fijo, por ATR, trailing, temporal), cuánto cuesta y cuál es su peor caso, y calcula el riesgo de ruina con fórmulas y con simulación, para ver dónde dejan de valer las fórmulas.

- Curso: [Nivel 4: Carteras, riesgo, ejecución y trading en real](https://pythonparatrading.com/curso-python-trading/nivel-4/)
- Módulo: 4.2 · Gestión del riesgo y tamaño de posición
- Duración: 65 min
- Requisitos: [2.2.2 · Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo](https://pythonparatrading.com/curso-python-trading/nivel-2/universo-senal-filtro-entrada-salida-y-sizing/), [2.4.8 · Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops](https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-fills/), [2.5.5 · Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales](https://pythonparatrading.com/curso-python-trading/nivel-2/monte-carlo-bootstrap-y-permutaciones/)

## Al terminar esta lección

- Dimensionar una operación por su riesgo (capital × f / distancia al stop) con contratos enteros, y medir cuánto se aleja el riesgo real del planeado.
- Calcular el ATR de Wilder, compararlo con el de TA-Lib y entender por qué un stop fijo en porcentaje cambia de significado con la volatilidad.
- Medir en un backtest con costes lo que aportan y lo que cuestan los stops fijos, por ATR, trailing y temporales, con un control de entradas al azar y datos sin tendencia.
- Calcular el riesgo de ruina con la fórmula del jugador, la de difusión y la de Lundberg, compararlas con una simulación y explicar por qué el riesgo por operación no es el riesgo de la cartera.

## El riesgo de una operación se decide antes de entrar

Un sistema con ventaja puede arruinar a su operador si el tamaño de cada operación es demasiado grande. La forma más
extendida de controlarlo es fijar de antemano cuánto se pierde si la operación sale mal: el **riesgo por operación**,
una fracción $f$ del capital (los manuales suelen hablar de un 0,5 % a un 2 %). A esa pérdida planeada se le llama
**R**, y los resultados de las operaciones se miden en múltiplos de R: una operación de +3 R gana el triple de lo que
se arriesgaba; una de −1 R pierde lo previsto. Para que la pérdida sea la prevista hace falta un **stop**, un precio
al que se cierra la posición, y el tamaño sale de la distancia hasta él:

$$
N = \left\lfloor \frac{K \cdot f}{d \cdot M \cdot P} \right\rfloor
$$

con $K$ el capital, $d$ la distancia al stop en tanto por uno del precio, $M$ el multiplicador del contrato y $P$ el
precio. El suelo es importante: con futuros solo se negocian contratos enteros.

```mermaid
flowchart TB
  S[Señal de entrada] --> D[Distancia al stop]
  V[ATR] --> D
  D --> N[Contratos]
  N --> O[Orden y stop]
  O --> R[Resultado en R]
```

Los datos son los futuros sintéticos del nivel 2 (24 mercados de 2000 a 2025, con la tendencia plantada de su
README: un 0,15·σ anual en la dirección de los últimos 12 meses). Solo tienen cierres, así que la volatilidad se mide
con un «ATR de cierres», la media de Wilder del cambio absoluto de cierre a cierre (la sección siguiente lo compara
con el ATR completo). Con un stop a 3 ATR de cierres y un 0,5 % de riesgo:

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

fut = pd.read_parquet("futuros2_continuos.parquet")
esp = pd.read_csv("futuros2_especificaciones.csv", index_col="mercado")
AJ = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")
P = fut.pivot(index="fecha", columns="mercado", values="cierre")
CONTRATO = fut.pivot(index="fecha", columns="mercado",
                     values="contrato")
esp = esp.loc[AJ.columns]
# «ATR de cierres»: media de Wilder de |ΔC| en % del precio
atr = AJ.diff().abs().ewm(alpha=1 / 20, adjust=False).mean() / AJ
K_STOP = 3.0                     # distancia del stop: 3 ATR de cierres

def contratos(capital, f, dia):
    riesgo = (K_STOP * atr.loc[dia] * P.loc[dia]
              * esp["multiplicador"])         # USD por contrato
    n = np.floor(capital * f / riesgo)
    return pd.DataFrame({"riesgo_contrato": riesgo,
                         "contratos": n,
                         "riesgo_real_%": 100 * n * riesgo / capital})

dia = "2025-12-31"
pd.concat({f"{c:,.0f}": contratos(c, 0.005, dia).loc[
    ["IDA", "BOA", "BOC", "ENA", "MEA", "AGA", "DIA"]]
    for c in (1_000_000, 100_000)}, axis=1).round(2)
```
```output
              1,000,000                                 100,000
        riesgo_contrato contratos riesgo_real_% riesgo_contrato contratos riesgo_real_%
mercado
IDA             9371.01       0.0          0.00         9371.01       0.0          0.00
BOA              405.08      12.0          0.49          405.08       1.0          0.41
BOC              660.84       7.0          0.46          660.84       0.0          0.00
ENA             2716.93       1.0          0.27         2716.93       0.0          0.00
MEA              764.58       6.0          0.46          764.58       0.0          0.00
AGA              528.64       9.0          0.48          528.64       0.0          0.00
DIA              924.37       5.0          0.46          924.37       0.0          0.00
```

Con un millón de USD, el presupuesto por operación es de 5.000 USD. El índice `IDA` tiene un riesgo de 9.371 USD
por contrato hasta el stop: no cabe ni uno, y el sistema no puede operarlo con ese riesgo. El crudo (`ENA`, 2.717 USD
por contrato) admite uno, con un riesgo real del 0,27 % en lugar del 0,5 %. Los demás quedan cerca del objetivo (del
0,46 % al 0,49 %). Con 100.000 USD solo el bono a 2 años (`BOA`, 405 USD por contrato) admite un contrato. Es el
problema de la granularidad de la lección 3.1.4, visto desde el riesgo por operación: con poco capital, la regla del
«1 % por operación» no se puede aplicar a contratos grandes, y las salidas son los micros, menos mercados o aceptar
riesgos desiguales. El sistema de la lección, con un millón, se salta las operaciones que no admiten un contrato.

## ATR: la distancia que se adapta

El ATR (*average true range*, Wilder, 1978; lección 1.7.1) es la media del **rango verdadero**: el máximo entre el
rango del día y la distancia desde el cierre anterior al máximo o al mínimo, para contar los huecos. Con barras
diarias de KAPA (el ETF de las barras de 5 minutos del nivel 2), el ATR de 14 sesiones de TA-Lib y uno propio con la
media de Wilder:

```python
import talib

b = pd.read_parquet("indice2_5min.parquet")
b["dia"] = b["ts"].dt.tz_convert("America/New_York").dt.date
d = b.groupby("dia").agg(apertura=("apertura", "first"),
                         maximo=("maximo", "max"),
                         minimo=("minimo", "min"),
                         cierre=("cierre", "last"))
d.index = pd.to_datetime(d.index)
d["atr"] = talib.ATR(d["maximo"], d["minimo"], d["cierre"],
                     timeperiod=14)
previo = d["cierre"].shift(1)
rango = pd.concat([d["maximo"] - d["minimo"],
                   (d["maximo"] - previo).abs(),
                   (d["minimo"] - previo).abs()], axis=1).max(axis=1)
propio = pd.Series(np.nan, index=d.index)
propio.iloc[14] = rango.iloc[1:15].mean()        # semilla de Wilder
for i in range(15, len(d)):
    propio.iloc[i] = (13 * propio.iloc[i - 1] + rango.iloc[i]) / 14
print(f"máxima diferencia TA-Lib - propio: "
      f"{(d['atr'] - propio).abs().max():.1e}")
print(f"ATR / media de |ΔC|: "
      f"{rango.mean() / d['cierre'].diff().abs().mean():.2f}")
(100 * d["atr"] / d["cierre"]).groupby(d.index.year).mean().round(2)
```
```output
máxima diferencia TA-Lib - propio: 5.3e-15
ATR / media de |ΔC|: 1.88
dia
2022    1.92
2023    1.34
2024    1.37
2025    1.58
dtype: float64
```

TA-Lib y la implementación propia coinciden hasta el redondeo (5e-15). Dos datos útiles. El rango verdadero medio es
1,88 veces el cambio absoluto medio de cierre a cierre: el «ATR de cierres» que usa el sistema de futuros es más
pequeño que el ATR completo, y 3 ATR de cierres equivalen a 1,6 ATR completos. Y el ATR, en porcentaje del precio,
varía de un año a otro: 1,92 % de media en 2022, 1,34 % en 2023, 1,37 % en 2024 y 1,58 % en 2025. Esa variación es la
que estropea un stop fijo:

```python
ayer = d.shift(1)
stops = pd.DataFrame({
    "fijo_2%": d["minimo"] <= ayer["cierre"] * 0.98,
    "1,3_ATR": d["minimo"] <= ayer["cierre"] - 1.3 * ayer["atr"]}
).loc[ayer["atr"].notna()]
distancia = (100 * 1.3 * ayer["atr"] / ayer["cierre"]).loc[stops.index]
pd.concat([stops.groupby(stops.index.year).mean().mul(100),
           distancia.groupby(distancia.index.year).mean()
           .rename("dist_ATR_%")], axis=1).round(2)
```
```output
      fijo_2%  1,3_ATR  dist_ATR_%
dia
2022     8.47     4.24        2.50
2023     4.00     7.20        1.74
2024     2.78     7.54        1.79
2025     5.20     4.80        2.05
```

Para una posición comprada al cierre anterior, un stop al 2 % se toca el 8,47 % de los días de 2022 y solo el 2,78 %
de los de 2024: el mismo stop es tres veces más «nervioso» en un año agitado. Un stop a 1,3 ATR (de media, unos 2 %)
se toca entre el 4,24 % y el 7,54 % de los días, porque su distancia se abre al 2,50 % en 2022 y se cierra al 1,74 %
en 2023. No es constante: el ATR se calcula con las 14 sesiones anteriores y llega tarde a los cambios de régimen. Pero
la diferencia de significado entre mercados, que es la grande, desaparece.

## Un sistema de tendencia y cinco salidas

Para medir los stops hace falta un sistema. El de la lección es deliberadamente simple: en cada uno de los 24
futuros, cuando la media exponencial de 20 sesiones cruza la de 100, se entra en la dirección del cruce y se sale al
cruce contrario (una entrada por tramo de señal). Todas las decisiones se toman con el cierre de ayer y se ejecutan al
cierre de hoy; el tamaño es el del 0,5 % de riesgo con el stop a 3 ATR de cierres, en contratos enteros; cada cambio
de posición paga los ticks de deslizamiento y la comisión de `futuros2_especificaciones.csv`, y cada roll, dos lados.
Las cinco variantes solo cambian la salida:

- **sin stop**: solo el cruce contrario;
- **fijo 5 %**: además, se sale si la operación pierde un 5 % del precio de entrada;
- **ATR 3**: se sale si pierde 3 ATR de cierres (el stop con el que se dimensiona: −1 R);
- **trailing 10 ATR**: se sale si el precio retrocede 10 ATR desde el mejor cierre de la operación;
- **tiempo 20**: se sale si a las 20 sesiones la operación no gana.

```python
r = AJ.pct_change()
senal = np.sign(AJ.ewm(span=20).mean() - AJ.ewm(span=100).mean())
CAPITAL, F = 1_000_000, 0.005
mult = esp["multiplicador"].to_numpy()
coste_lado = (esp["coste_tipico_ticks"] * esp["valor_tick"]
              + esp["comision_usd"]).to_numpy()
cambio_ctr = (CONTRATO.ne(CONTRATO.shift(1))
              & CONTRATO.shift(1).notna())

def sistema(salida="ninguna", k=3.0, fijo=0.05, n=20, azar=False,
            datos=None, semilla=1, plazo=60):
    """Tendencia (cruce de medias 20/100) en 24 futuros. Decide con
    el cierre de ayer y opera al cierre de hoy. Tamaño: 0,5 % del
    capital inicial hasta un stop a K_STOP ATR (contratos enteros).
    salida: 'ninguna' (solo la señal contraria), 'fijo' (pérdida
    del `fijo` %), 'atr' (k ATR desde la entrada), 'trailing'
    (k ATR desde el mejor cierre) o 'tiempo' (n sesiones sin
    ganar). azar=True: entradas y lados al azar, salida a `plazo`."""
    Aj, Sg, At = datos if datos is not None else (AJ, senal, atr)
    rng = np.random.default_rng(semilla)
    a, s, v, p = (Aj.to_numpy(), Sg.to_numpy(), At.to_numpy(),
                  P.to_numpy())
    T, M = a.shape
    N, ops = np.zeros((T, M)), []
    for j in range(M):
        lado = 0
        hecho = 0
        for t in range(120, T):
            if lado:
                N[t, j] = lado * nc          # mantenida en la sesión t
                mov = (a[t - 1, j] / e - 1) * lado
                dias = t - 1 - t0
                motivo = None
                if azar and dias >= plazo:
                    motivo = "plazo"
                elif not azar and s[t - 1, j] != lado:
                    motivo = "señal"
                if salida == "fijo" and mov <= -fijo:
                    motivo = "stop"
                if salida == "atr" and mov <= -k * a0:
                    motivo = "stop"
                if salida == "trailing":
                    mejor = max(mejor, mov)
                    if mov <= mejor - k * a0:
                        motivo = "stop"
                if salida == "tiempo" and dias >= n and mov <= 0:
                    motivo = "tiempo"
                if motivo is None:
                    continue
                ops.append((j, t0, t, lado, nc, motivo))  # sale en t
                lado = 0
            if np.isnan(s[t - 1, j]) or np.isnan(v[t - 1, j]):
                continue
            if azar:
                entra = rng.random() < 1 / 40
            else:                    # una entrada por tramo de señal
                entra = s[t - 1, j] != 0 and s[t - 1, j] != hecho
            if not entra:
                continue
            a0 = v[t - 1, j]
            nc = np.floor(CAPITAL * F
                          / (K_STOP * a0 * p[t - 1, j] * mult[j]))
            if nc < 1:
                continue
            lado = (int(rng.choice([-1, 1])) if azar
                    else int(s[t - 1, j]))
            hecho = s[t - 1, j]
            e, t0, mejor = a[t, j], t, 0.0     # entra al cierre de t
    N = pd.DataFrame(N, index=AJ.index, columns=AJ.columns)
    rr = Aj.pct_change()
    pyg = (N * mult * P.shift(1) * rr).fillna(0)
    operado = N.diff().abs().shift(-1).fillna(0)  # al cierre de t
    coste = operado * coste_lado + 2 * N.abs() * cambio_ctr * coste_lado
    neto = (pyg.sum(axis=1) - coste.sum(axis=1)) / CAPITAL
    ops = pd.DataFrame(ops, columns=["j", "t0", "t", "lado", "nc",
                                     "motivo"])
    ops["mercado"] = AJ.columns[ops["j"]]
    ops["R"] = [pyg.iloc[i + 1:k_ + 1, j].sum() / (CAPITAL * F)
                for j, i, k_ in zip(ops["j"], ops["t0"], ops["t"])]
    ops["dias"] = ops["t"] - ops["t0"]
    return neto.loc["2001":], ops

def resumen(neto, ops):
    eq = (1 + neto).cumprod()
    dd = eq / eq.cummax() - 1
    R = ops["R"]
    return {"ops": len(ops), "gan_%": 100 * (R > 0).mean(),
            "E[R]": R.mean(), "peor_R": R.min(),
            "p1_R": R.quantile(0.01),
            "stop_%": 100 * (ops["motivo"] != "señal").mean(),
            "sharpe": neto.mean() / neto.std() * np.sqrt(252),
            "caida_%": 100 * dd.min(),
            "peor_mes_%": 100 * neto.resample("ME").sum().min()}
```

El P&L se calcula contrato a contrato en dólares (contratos × multiplicador × precio de ayer × rendimiento de hoy de
la serie ajustada) y el R de cada operación es su resultado bruto dividido por los 5.000 USD de riesgo planeado.
Las cinco salidas:

```python
SALIDAS = {"sin stop": {},
           "fijo 5 %": {"salida": "fijo"},
           "ATR 3": {"salida": "atr", "k": 3},
           "trailing 10 ATR": {"salida": "trailing", "k": 10},
           "tiempo 20": {"salida": "tiempo", "n": 20}}
RES = {k: sistema(**kw) for k, kw in SALIDAS.items()}
pd.DataFrame({k: resumen(*v) for k, v in RES.items()}).T.round(2)
```
```output
                    ops  gan_%  E[R]  peor_R  p1_R  stop_%  sharpe  caida_%  peor_mes_%
sin stop         1841.0  31.29  0.43   -4.63 -3.56    0.00    0.67   -42.73      -19.69
fijo 5 %         1846.0  28.01  0.38   -4.51 -3.16   31.53    0.63   -32.44      -18.06
ATR 3            1849.0  24.07  0.29   -3.70 -2.44   62.57    0.54   -31.59      -16.21
trailing 10 ATR  1842.0  34.09  0.28   -4.25 -3.42   45.82    0.56   -43.00      -14.05
tiempo 20        1851.0  29.23  0.26   -4.25 -3.36   50.08    0.48   -30.27      -13.79
```

La figura de la cabecera resume la tabla. Sin stop, el sistema hace 1.841 operaciones en 25 años; solo el 31,3 %
gana, pero la esperanza es de +0,43 R por operación (las ganadoras son grandes: es una tendencia), con un Sharpe neto
de 0,67, una caída máxima del 42,7 % y un peor mes del −19,7 %. **Cada stop cuesta Sharpe**: 0,63 el fijo del 5 %,
0,54 el de 3 ATR, 0,56 el trailing y 0,48 el temporal, porque cada uno corta operaciones que, aguantando, habrían
terminado bien. Lo que compran es una cola más corta: con el stop de 3 ATR, el percentil 1 por operación pasa de
−3,56 R a −2,44 R, la caída máxima del 42,7 % al 31,6 % y el peor mes del −19,7 % al −16,2 %. El stop temporal da la
caída más pequeña (30,3 %) y el peor mes más suave (−13,8 %), con el peor Sharpe.

No hay una salida «mejor»: hay un intercambio entre rentabilidad y forma de las pérdidas. Lo que sí hay es una regla
que no conviene seguir, la del stop fijo en porcentaje del precio, y se ve por sectores:

```python
o = RES["fijo 5 %"][1]
o["sector"] = esp.loc[o["mercado"], "sector"].to_numpy()
o.groupby("sector").agg(
    operaciones=("R", "size"),
    stop_pct=("motivo", lambda m: 100 * (m == "stop").mean()),
    E_R=("R", "mean")).round(2)
```
```output
                operaciones  stop_pct   E_R
sector
agricolas               313     50.80  0.42
bonos                   401      0.25  0.38
divisas                 268      7.46  0.00
energia                 277     66.43 -0.01
metales                 338     39.35  0.31
renta_variable          249     34.14  1.25
```

El mismo 5 % no se toca nunca en los bonos (un 0,25 % de las operaciones, con volatilidades del 2 % al 12 % anual) y
corta dos de cada tres operaciones en energía (66,4 %, con volatilidades del 33 % al 50 %). En bonos es como no tener
stop; en energía es una salida casi aleatoria, y la esperanza de esas operaciones queda en −0,01 R. Un stop tiene que
medirse en unidades de la volatilidad del mercado, que es lo que hacen el de ATR y el trailing.

## El control: ¿crea ventaja un stop?

Una idea muy repetida es que un buen stop convierte cualquier entrada en rentable («corta las pérdidas y deja correr
las ganancias»). Se puede comprobar con dos controles: entradas al azar (cada día, una probabilidad de 1/40 de entrar
en cada mercado, en un lado al azar, y salida a las 60 sesiones si no salta antes el stop) y los mismos futuros con
los rendimientos diarios **permutados** dentro de cada mercado, que conservan la distribución de cada día y destruyen
la tendencia (como en la lección 2.5.5):

```python
rng = np.random.default_rng(11)
perm = r.copy()
for m in perm.columns:
    v = perm[m].dropna()
    perm.loc[v.index, m] = rng.permutation(v.to_numpy())
AJp = ((1 + perm.fillna(0)).cumprod() * AJ.bfill().iloc[0]).where(
    AJ.notna())
sin_tend = (AJp,
            np.sign(AJp.ewm(span=20).mean()
                    - AJp.ewm(span=100).mean()),
            AJp.diff().abs().ewm(alpha=1 / 20, adjust=False).mean()
            / AJp)
filas = {}
for nombre, kw in [("señal", {}), ("señal + ATR 3", {"salida": "atr"}),
                   ("azar", {"azar": True}),
                   ("azar + ATR 3", {"azar": True, "salida": "atr"}),
                   ("azar + trailing", {"azar": True, "k": 10,
                                        "salida": "trailing"})]:
    m_real = resumen(*sistema(**kw))
    m_perm = resumen(*sistema(datos=sin_tend, **kw))
    filas[nombre] = {"E[R]": m_real["E[R]"], "sharpe": m_real["sharpe"],
                     "peor_R": m_real["peor_R"],
                     "E[R]_sin_tend": m_perm["E[R]"],
                     "sharpe_sin_tend": m_perm["sharpe"]}
pd.DataFrame(filas).T.round(2)
```
```output
                 E[R]  sharpe  peor_R  E[R]_sin_tend  sharpe_sin_tend
señal            0.43    0.67   -4.63           0.00            -0.13
señal + ATR 3    0.29    0.54   -3.70          -0.00            -0.14
azar            -0.02   -0.15  -16.80          -0.02            -0.25
azar + ATR 3     0.10    0.31   -3.65          -0.04            -0.37
azar + trailing  0.21    0.47   -6.26          -0.06            -0.36
```

Con los datos reales del curso, las entradas al azar sin stop dan −0,02 R por operación y un Sharpe de −0,15 (los
costes), con una peor operación de −16,8 R: sin stop y con 60 sesiones de plazo, una operación al azar puede salir
muy mal. Con un stop de 3 ATR, las mismas entradas al azar ganan +0,10 R y un Sharpe de 0,31; con un trailing de 10
ATR, +0,21 R y 0,47. ¿Crean ventaja los stops? Con los rendimientos permutados, sin tendencia, la respuesta es no:
todas las variantes pierden (de −0,00 a −0,06 R, Sharpe de −0,13 a −0,37), y los stops no arreglan las entradas al
azar. Lo que pasa en los datos reales es que **un stop es una regla de tendencia**: cerrar lo que va mal y mantener lo
que va bien es apostar a que los movimientos continúan, y en datos con tendencia plantada eso gana. En un mercado sin
tendencia (o con reversión a la media, lección 2.3.2), el mismo stop pierde. Antes de atribuir el resultado de un
sistema a su stop, conviene hacer este control.

## El peor episodio: más de 1 R

El stop de 3 ATR define −1 R. La figura enseña cómo queda la distribución de los resultados con y sin él:

*Fig. — Distribución del resultado por operación, en múltiplos del riesgo planeado R, del sistema de tendencia sin stop y con un stop de 3 ATR (1 R) en 24 futuros (2001-2025); las operaciones de más de 10 R van en la última barra.* Dos histogramas en barras de medio R, de −5 R a 10 R. Arriba, sin stop: las pérdidas se reparten entre 0 y −4,6 R y las ganancias tienen una cola larga, con 67 operaciones de más de 10 R. Abajo, con un stop de 3 ATR marcado en −1 R (línea destacada): las pérdidas se acumulan entre 0 y −1,5 R, pero quedan operaciones hasta −3,7 R, y la cola de ganancias se mantiene, con 49 operaciones de más de 10 R.

Sin stop, las pérdidas se reparten entre 0 y −4,6 R; con él, se amontonan entre 0 y −1,5 R. La cola derecha, la que
paga el sistema (49 operaciones de más de 10 R con stop, 67 sin él), apenas cambia. Pero el stop no corta la cola
izquierda en −1 R:

```python
o = RES["ATR 3"][1]
fechas = AJ.index
peores = o.nsmallest(5, "R").assign(
    entrada=lambda x: fechas[x["t0"]].date,
    salida=lambda x: fechas[x["t"]].date)
print(peores[["mercado", "lado", "entrada", "salida", "motivo",
              "R"]].round(2).to_string(index=False))
stop = o[o["motivo"] == "stop"]["R"]
print(f"salidas por stop: {len(stop)}, R medio {stop.mean():.2f}, "
      f"peor que -1,5 R: {(stop < -1.5).mean():.1%}")
```
```output
mercado  lado    entrada     salida motivo     R
    MEC    -1 2002-02-06 2002-05-13   stop -3.70
    BOA     1 2023-10-27 2023-10-31   stop -3.62
    AGD    -1 2024-07-16 2024-08-13   stop -3.13
    MEC     1 2024-07-19 2024-11-01   stop -3.09
    BOD     1 2023-10-05 2023-10-31   stop -2.90
salidas por stop: 1157, R medio -1.07, peor que -1,5 R: 16.8%
```

De 1.157 salidas por stop, el resultado medio es −1,07 R, pero el 16,8 % pierde más de 1,5 R y la peor, −3,70 R
(una venta de cobre en 2002). La segunda es instructiva: una compra del bono a 2 años el 27 de octubre de 2023, con
el ATR en mínimos (un 0,075 % diario), que en dos sesiones cae unos 6 y 5 ATR de los de la entrada; el stop se detecta al cierre y se ejecuta
al siguiente, y la pérdida llega a 3,62 R. Es el **peor episodio** de la regla: un stop en ATR es más estrecho
justo cuando la volatilidad ha estado baja, y un cambio de régimen lo atraviesa. Con stops intradía pasa lo mismo con
los huecos de apertura (lección 2.4.8). Un stop limita la pérdida típica, no la máxima; el dimensionamiento tiene que
contar con pérdidas de varios R.

## Riesgo de ruina: fórmulas

El **riesgo de ruina** es la probabilidad de perder una parte del capital que obliga a parar: todo, la mitad o la
caída que el operador o sus clientes no aguantan. La fórmula clásica es la del jugador: con apuestas de una unidad,
probabilidad $p > \tfrac12$ de ganar una unidad y $1-p$ de perderla, y un capital de $U$ unidades, la probabilidad de
perderlo todo alguna vez es

$$
P_{\text{ruina}} = \left(\frac{1-p}{p}\right)^{U}
$$

```python
def ruina_jugador(p, unidades):
    """P(perderlo todo) apostando 1 unidad a cara o cruz con P(ganar)
    = p > 0,5 frente a un rival infinito."""
    return ((1 - p) / p) ** unidades

rng = np.random.default_rng(5)
p, U, caminos, pasos = 0.55, 10, 20_000, 2_000
x = np.where(rng.random((caminos, pasos)) < p, 1, -1)
minimo = U + np.minimum.accumulate(x.cumsum(axis=1), axis=1)[:, -1]
print(f"fórmula {ruina_jugador(p, U):.4f}, "
      f"simulación {(minimo <= 0).mean():.4f}")
```
```output
fórmula 0.1344, simulación 0.1333
```

Con un 55 % de aciertos y diez unidades, la probabilidad de ruina es del 13,4 %, y 20.000 caminos simulados de 2.000
apuestas dan un 13,3 %. Un trader no apuesta unidades fijas: arriesga una fracción $f$ del capital **actual**, y
entonces el capital se multiplica por $(1 + f\,R_i)$ en cada operación. En logaritmos es un paseo aleatorio con
pasos $x_i = \ln(1 + f R_i)$, y perder la mitad del capital es bajar $D = \ln 2$. Dos fórmulas:

- **Difusión**: si los pasos fueran normales, con media $\mu$ y varianza $\sigma^2$,
  $P_{\text{ruina}} = e^{-2\mu D/\sigma^2}$.
- **Lundberg** (Cramér-Lundberg): con $s > 0$ la raíz de $\mathbb{E}[e^{-s\,x}] = 1$, $P_{\text{ruina}} \le
  e^{-s D}$. No supone normalidad: usa la distribución entera de los pasos. Es una cota superior, que se alcanza
  cuando la caída no se pasa de largo al cruzar el umbral.

Con los resultados en R de las 1.849 operaciones del sistema con stop de 3 ATR, y una simulación que los remuestrea
como si fueran independientes (1.000 operaciones, 4.000 caminos):

```python
from scipy.optimize import brentq

R = RES["ATR 3"][1]["R"].to_numpy()
L = 0.5                        # «ruina»: perder la mitad del capital
D = -np.log(L)

def ruina_formulas(f):
    x = np.log1p(f * R)        # crecimiento log por operación
    difusion = np.exp(-2 * x.mean() * D / x.var())
    s = brentq(lambda s: np.mean(np.exp(-s * x)) - 1, 1e-6, 500)
    return difusion, np.exp(-s * D)

def ruina_simulada(f, n=1000, caminos=4000, semilla=3, nivel=L):
    """Operaciones remuestreadas (independientes) con riesgo f."""
    rng = np.random.default_rng(semilla)
    capital = np.cumprod(1 + f * rng.choice(R, (caminos, n)), axis=1)
    desde_inicio = (capital.min(axis=1) <= nivel).mean()
    pico = np.maximum.accumulate(np.maximum(capital, 1), axis=1)
    desde_pico = ((capital / pico).min(axis=1) <= nivel).mean()
    return desde_inicio, desde_pico

filas = {}
for f in (0.005, 0.01, 0.02, 0.03, 0.05):
    dif, lund = ruina_formulas(f)
    ini, pico = ruina_simulada(f)
    filas[f"{100 * f:g} %"] = {"difusion": dif, "lundberg": lund,
                               "simulada": ini, "caida_50%_pico": pico}
print(f"{len(R)} operaciones: E[R] {R.mean():.3f}, "
      f"desv. {R.std():.2f}, ganadoras {(R > 0).mean():.1%}")
pd.DataFrame(filas).T.round(4)
```
```output
1849 operaciones: E[R] 0.292, desv. 3.60, ganadoras 24.1%
       difusion  lundberg  simulada  caida_50%_pico
0.5 %    0.0022    0.0004    0.0002          0.0015
1 %      0.0531    0.0256    0.0272          0.2218
2 %      0.2682    0.2106    0.1955          0.9445
3 %      0.4719    0.4267    0.4132          1.0000
5 %      0.7660    0.7536    0.7040          1.0000
```

*Fig. — Probabilidad de ruina en 1.000 operaciones según el riesgo por operación, con los resultados en R del sistema con stop de 3 ATR remuestreados (operaciones independientes).* Tres líneas frente al riesgo por operación (0,5 %, 1 %, 2 %, 3 % y 5 % del capital). Probabilidad simulada de perder la mitad del capital inicial: 0,02 %, 2,7 %, 19,6 %, 41,3 % y 70,4 %. La fórmula de Lundberg da casi lo mismo: 0,04 %, 2,6 %, 21,1 %, 42,7 % y 75,4 %. Probabilidad de una caída del 50 % desde un máximo (destacada): 0,15 %, 22,2 %, 94,5 % y 100 % a partir del 3 %.

Las operaciones tienen una esperanza de 0,292 R con una desviación de 3,60 R y solo un 24,1 % de ganadoras: una
distribución muy asimétrica. La fórmula de Lundberg y la simulación coinciden dentro del error de la simulación
(unos ±0,3 puntos con 4.000 caminos al 1 %): 2,6 % y 2,7 % de probabilidad de perder la mitad del capital arriesgando
un 1 % por operación; 21 % y 20 % con un 2 %; 75 % y 70 % con un 5 %. La de difusión exagera (5,3 % al 1 %): trata la
distribución como normal y la gran cola de ganancias, que aleja la ruina, entra solo a través de la varianza, como si
fuera riesgo.

La última columna es la que más importa en la práctica: la probabilidad de que en algún momento el capital caiga un
50 % **desde un máximo**, no desde el inicio. Al 1 % es del 22 %; al 2 %, del 94 %. La ruina desde el capital inicial
se aleja a medida que se gana; la caída desde un máximo no, y es la que sufre quien empezó a operar en un buen
momento. Las cifras dependen de 1.000 operaciones: con más operaciones, la probabilidad desde el máximo sube.

## Las operaciones no van de una en una

Las fórmulas y la simulación anterior tratan las operaciones en fila: una termina y empieza la siguiente. En el
sistema de la lección hay muchas abiertas a la vez:

```python
o = RES["ATR 3"][1]
abiertas = np.zeros(len(AJ))
for i, k_ in zip(o["t0"], o["t"]):
    abiertas[i + 1:k_ + 1] += 1
abiertas = pd.Series(abiertas, AJ.index).loc["2001":]
print(f"operaciones abiertas: media {abiertas.mean():.1f}, "
      f"máximo {abiertas.max():.0f}")
print(f"riesgo abierto medio {0.5 * abiertas.mean():.1f} % del "
      f"capital, máximo {0.5 * abiertas.max():.0f} %")
_, p30 = ruina_simulada(0.005, n=len(R), nivel=0.7)
eq = (1 + RES["ATR 3"][0]).cumprod()
print(f"en serie: P(caída del 30 % desde el máximo) = {p30:.3f}; "
      f"real: {100 * (eq / eq.cummax() - 1).min():.1f} %")
```
```output
operaciones abiertas: media 16.7, máximo 24
riesgo abierto medio 8.4 % del capital, máximo 12 %
en serie: P(caída del 30 % desde el máximo) = 0.308; real: -31.6 %
```

De media hay 16,7 operaciones abiertas, y hasta 24 (una por mercado). El riesgo abierto (la suma de los R de las
posiciones vivas) es de un 8,4 % del capital de media y llega al 12 %: el «0,5 % por operación» es en realidad un
riesgo de cartera que depende de cuántas posiciones hay y de cómo se mueven juntas. En este sistema, la caída real de
31,6 % cabe en lo que da la simulación en serie con las mismas 1.849 operaciones (un 30,8 % de probabilidad de una
caída del 30 % desde un máximo), a diferencia de la lección 2.5.5, donde la secuencia real era peor que cualquier
reordenación. Pero el tiempo es otro: en serie, esas 1.849 operaciones se suceden; en la cuenta, se acumulan en los
mismos meses, y el peor mes del sistema pierde un 16,2 %. Por eso el dimensionamiento de una cartera se hace con la
volatilidad conjunta de las posiciones (volatility targeting, lección 4.2.2) y se vigila con límites de caída (4.2.5),
no solo con el riesgo de cada operación.

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

Los futuros son sintéticos y la tendencia está plantada: que el sistema gane valida el código, no la estrategia. Lo
que sí se traslada a los mercados reales es la mecánica: los stops cambian la forma de la distribución y cuestan
rentabilidad, los huecos y los cambios de volatilidad los atraviesan, y el riesgo de ruina depende de la definición
de ruina (desde el inicio o desde un máximo) y de cuántas operaciones hay abiertas a la vez.

</aside>

## Errores típicos

- **Dimensionar por el stop y olvidar el redondeo.** Con contratos enteros, el riesgo real puede ser la mitad del
  planeado o, si se redondea hacia arriba, el doble.
- **Usar el mismo stop en porcentaje para todos los mercados.** Mide la volatilidad, no la idea.
- **Atribuir al stop la ventaja del sistema.** Haz el control con entradas al azar y con datos sin tendencia.
- **Suponer que −1 R es la pérdida máxima.** Los huecos y la ejecución al cierre siguiente la superan.
- **Calcular la ruina desde el capital inicial y con operaciones en fila.** La caída desde un máximo y las posiciones
  simultáneas dan probabilidades mucho mayores.

## Ejercicios

1. Repite el sistema con un stop de 2 ATR y otro de 5 ATR. ¿Cómo cambian el Sharpe, el percentil 1 y la caída
   máxima? ¿Hay un punto en que el stop deja de costar?

**Solución.**

```python
pd.DataFrame({f"ATR {k}": resumen(*sistema(salida="atr", k=k))
              for k in (2, 3, 5)}).T[
    ["gan_%", "E[R]", "p1_R", "stop_%", "sharpe", "caida_%"]].round(2)
```
```output
       gan_%  E[R]  p1_R  stop_%  sharpe  caida_%
ATR 2  21.36  0.23 -1.97   73.95    0.47   -34.09
ATR 3  24.07  0.29 -2.44   62.57    0.54   -31.59
ATR 5  29.27  0.36 -2.82   37.45    0.60   -34.67
```

Con 2 ATR, el stop salta en el 74 % de las operaciones, el percentil 1 baja a −1,97 R y el Sharpe a 0,47; con 5 ATR
salta en el 37 %, el Sharpe sube a 0,60 y el percentil 1 empeora a −2,82 R. La caída máxima no sigue a la anchura: es
del 34,1 % con 2 ATR, del 31,6 % con 3 y del 34,7 % con 5, porque la peor racha del sistema depende de muchas
operaciones simultáneas y no solo de cada stop. En este sistema el stop no deja de costar a ninguna anchura: cuanto
más ancho, más se parece al sistema sin stop (Sharpe 0,67) y menos recorta la cola. La anchura es una elección entre
rentabilidad y forma de las pérdidas, y elegirla mirando el backtest es otra fuente de sobreajuste (lección 2.5.6).

2. Calcula con las fórmulas de Lundberg y de difusión el riesgo por operación que da un 1 % de probabilidad de perder
   la mitad del capital con las operaciones del sistema con stop de 3 ATR.

**Solución.**

```python
for i, nombre in enumerate(["difusión", "Lundberg"]):
    f_max = brentq(lambda f: ruina_formulas(f)[i] - 0.01, 0.004, 0.05)
    print(f"{nombre:9s}: f = {100 * f_max:.2f} %, simulada "
          f"{ruina_simulada(f_max)[0]:.4f}")
```
```output
difusión : f = 0.66 %, simulada 0.0027
Lundberg : f = 0.82 %, simulada 0.0118
```

Para un 1 % de probabilidad de perder la mitad del capital (en 1.000 operaciones independientes), la fórmula de
Lundberg permite arriesgar un 0,82 % por operación y la de difusión, más prudente por su error, un 0,66 %. La
simulación con el 0,82 % da un 1,18 %, cerca del objetivo (la diferencia es el error de la simulación y el horizonte
finito); con el 0,66 %, un 0,27 %. Son cifras para operaciones en fila: con 17 posiciones abiertas a la vez, el riesgo
de la cartera manda, y por eso este cálculo es un techo para el riesgo por operación, no una receta.

3. Un curso en internet promete que «con un stop siempre a 1:3 (arriesgar 1 para ganar 3) y un 40 % de aciertos se
   gana siempre». ¿Qué supone y qué omite?

**Solución.**

Supone que cada operación termina exactamente en −1 R o en +3 R, que los aciertos son independientes y que su
probabilidad es del 40 %. Con esos números la esperanza es 0,4·3 − 0,6·1 = +0,6 R por operación, y la ruina con
riesgo fijo pequeño es improbable. Omite casi todo lo que mide esta lección: que las pérdidas pasan de −1 R (huecos,
ejecución al cierre siguiente: aquí, un 16,8 % de las salidas por stop pasan de −1,5 R); que el 40 % de aciertos con
un objetivo a +3 R no lo da la regla de salida sino la entrada (con entradas al azar y sin tendencia, la esperanza es
negativa por los costes); que las operaciones se solapan y se correlacionan; y que «siempre» no existe: incluso con
esperanza positiva, una caída del 50 % desde un máximo tiene una probabilidad alta si el riesgo por operación es del 2
% o más.

## Autoevaluación

1. Con 100.000 USD y un 0,5 % de riesgo por operación (500 USD), el riesgo de un contrato hasta el stop es de 660 USD. ¿Cuántos contratos se compran?

   - A. Uno: se redondea hacia arriba
   - B. 0,76 contratos
   - C. Ninguno: con contratos enteros hacia abajo, el riesgo de un contrato ya supera el presupuesto; o se renuncia a la operación o se acepta más riesgo del planeado
   - D. Depende del margen

   *Respuesta C: Ninguno: con contratos enteros hacia abajo, el riesgo de un contrato ya supera el presupuesto; o se renuncia a la operación o se acepta más riesgo del planeado.* floor(500 / 660) = 0. Con 100.000 USD, en la lección solo uno de los siete mercados de la tabla admite un contrato. Con poco capital, el riesgo por operación de los manuales no se puede aplicar a futuros grandes: hay que usar micros o aceptar posiciones de riesgo desigual.

2. En KAPA, un stop fijo del 2 % bajo el cierre anterior se toca el 8,5 % de los días de 2022 y el 2,8 % de los de 2024. ¿Por qué?

   - A. Porque 2022 tuvo más sesiones
   - B. Porque la volatilidad diaria (ATR medio del 1,92 % en 2022 y del 1,37 % en 2024) cambia y un stop en porcentaje fijo queda relativamente más cerca en los años agitados
   - C. Porque el stop está mal calculado
   - D. Por los huecos de apertura

   *Respuesta B: Porque la volatilidad diaria (ATR medio del 1,92 % en 2022 y del 1,37 % en 2024) cambia y un stop en porcentaje fijo queda relativamente más cerca en los años agitados.* Un 2 % es poco más de un ATR en 2022 y casi un ATR y medio en 2024. Un stop a 1,3 ATR mantiene una distancia proporcional a la volatilidad reciente, y su frecuencia de disparo varía menos entre años (del 4,2 % al 7,5 %), aunque no es constante porque el ATR se mueve con retraso.

3. En el sistema de tendencia, el stop de 3 ATR baja el Sharpe de 0,67 a 0,54. ¿Qué compra a cambio?

   - A. Una cola de pérdidas más corta: el percentil 1 por operación pasa de −3,56 R a −2,44 R, la caída máxima del 42,7 % al 31,6 % y el peor mes del −19,7 % al −16,2 %
   - B. Nada: es peor en todo
   - C. Más rentabilidad media
   - D. Menos operaciones

   *Respuesta A: Una cola de pérdidas más corta: el percentil 1 por operación pasa de −3,56 R a −2,44 R, la caída máxima del 42,7 % al 31,6 % y el peor mes del −19,7 % al −16,2 %.* El stop corta las operaciones que se tuercen, y con ellas algunas que habrían acabado bien (la tendencia necesita aguantar retrocesos): la esperanza por operación baja de 0,43 R a 0,29 R. A cambio, el peor caso es menos malo. Si eso compensa depende de lo que cueste al operador (o a sus clientes) una caída del 40 %.

4. Con un stop de 3 ATR, ¿por qué la peor operación pierde 3,7 R y no 1 R?

   - A. Por un error del backtest
   - B. Porque los costes son de 2,7 R
   - C. Porque el stop solo se aplica a las compras
   - D. Porque el stop se comprueba al cierre y se ejecuta al cierre siguiente, y en ese tiempo el precio puede seguir moviéndose muchos ATR, sobre todo si la volatilidad estaba baja al entrar

   *Respuesta D: Porque el stop se comprueba al cierre y se ejecuta al cierre siguiente, y en ese tiempo el precio puede seguir moviéndose muchos ATR, sobre todo si la volatilidad estaba baja al entrar.* Un stop limita la pérdida esperada, no la máxima. En la lección, el 16,8 % de las salidas por stop pierde más de 1,5 R; en el bono a 2 años, con un ATR muy bajo al entrar, dos sesiones de unos 6 y 5 ATR llevaron la pérdida a 3,6 R. Con stops intradía pasa lo mismo con los huecos (lección 2.4.8).

5. La fórmula de Lundberg y la simulación coinciden: arriesgando un 1 % por operación, la probabilidad de perder la mitad del capital es de un 2,6-2,7 %. ¿Por qué no basta para dimensionar?

   - A. Porque las fórmulas no sirven
   - B. Porque el 1 % es demasiado poco
   - C. Porque la simulación tiene pocos caminos
   - D. Porque suponen operaciones independientes y de una en una; el sistema tiene 16,7 abiertas de media, y una caída del 50 % desde un máximo (no desde el inicio) tiene una probabilidad del 22 % con ese mismo 1 %

   *Respuesta D: Porque suponen operaciones independientes y de una en una; el sistema tiene 16,7 abiertas de media, y una caída del 50 % desde un máximo (no desde el inicio) tiene una probabilidad del 22 % con ese mismo 1 %.* La ruina «desde el capital inicial» se aleja a medida que se gana; la caída desde un máximo, no. Y el riesgo por operación multiplicado por las operaciones abiertas a la vez (un 8,4 % de media con el 0,5 %) es el que sufre la cuenta en un mal mes. La lección 4.2.2 dimensiona con la volatilidad de la cartera.

## Ideas clave

- El tamaño por riesgo es capital × f / (distancia al stop × multiplicador × precio), en contratos enteros: con un millón de USD y un 0,5 %, el índice de la lección no admite ni un contrato; con 100.000 USD, solo el bono a 2 años.
- Un stop fijo en porcentaje cambia de significado con la volatilidad (se dispara del 0,25 % de las operaciones en bonos al 66 % en energía); un stop en múltiplos de ATR se adapta a cada mercado y a cada régimen.
- Los stops no crean ventaja: en el sistema de tendencia bajan el Sharpe (0,67 sin stop, 0,54 con 3 ATR) y acortan la cola (percentil 1 de −3,56 R a −2,44 R, caída máxima del 42,7 % al 31,6 %). Con entradas al azar, un stop parece ganar solo si los datos tienen tendencia; sin tendencia, pierde los costes.
- Un stop limita la pérdida típica, no la peor: con stop de 3 ATR al cierre, la peor operación pierde 3,7 R y el 16,8 % de las salidas por stop pasa de 1,5 R.
- Las fórmulas de ruina (jugador, difusión, Lundberg) coinciden con la simulación si se cumplen sus supuestos; la de difusión exagera con colas gruesas. Con un 1 % por operación, perder la mitad del capital inicial tiene un 2,7 % de probabilidad, pero una caída del 50 % desde un máximo, un 22 %.

## Recursos

### Research

- [Procesos Estocásticos - Paseos Aleatorios](https://pythonparatrading.com/procesos-estocasticos/) · Research-0005 · 12 min
- [Volatility Targeting en Python](https://pythonparatrading.com/volatility-targeting/) · Research-0004 · 8 min

### Documentación

- [TA-Lib para Python: indicadores de volatilidad (ATR, NATR, TRANGE)](https://ta-lib.github.io/ta-lib-python/func_groups/volatility_indicators.html) · ta-lib.github.io
- [SciPy: scipy.optimize.brentq](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brentq.html) · docs.scipy.org

### Datos

- [Datos del nivel 2: futuros continuos (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Descarga · futuros2_continuos.parquet
- [Datos del nivel 2: barras de 5 minutos de KAPA (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/indice2_5min.parquet) · Descarga · indice2_5min.parquet

Lección anterior: [4.1.8 · Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt](https://pythonparatrading.com/curso-python-trading/nivel-4/riskfolio-pyportfolioopt-skfolio-y-bt/)

Lección siguiente: [4.2.2 · Criterio de Kelly y volatility targeting: cuánto arriesgar](https://pythonparatrading.com/curso-python-trading/nivel-4/kelly-y-volatility-targeting/)
