Lección 4.1.7 · 65 min

Rebalanceo y carteras de estrategias

Unos pesos óptimos solo lo son el día en que se fijan: al día siguiente los precios ya los han movido. Esta lección mide qué cuesta devolverlos a su sitio, cada cuánto conviene hacerlo y qué se arriesga si no se hace, primero en una cartera de cinco ETF y después en una cartera de nueve estrategias, donde los pesos se estiman con datos del pasado y se rehacen cada mes.

Fig. — Peso de la renta variable (objetivo 50 %) en una cartera de cinco activos sin rebalancear y con una banda de 5 puntos por activo, a fin de mes (2006-2025)
Lecciones del nivel 4
  1. 4.1 · Teoría y optimización de carteras

    1. 4.1.1 Markowitz: frontera eficiente y restricciones reales
    2. 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
    3. 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
    4. 4.1.4 Risk parity, mínima varianza, HRP y HERC
    5. 4.1.5 Optimización con CVaR, drawdown, rotación y costes
    6. 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
    7. 4.1.7 Rebalanceo y carteras de estrategias
    8. 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
  2. 4.2 · Gestión del riesgo y tamaño de posición

    1. 4.2.1 Riesgo por operación, stops y riesgo de ruina
    2. 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
    3. 4.2.3 VaR, Expected Shortfall y backtesting del VaR
    4. 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
    5. 4.2.5 Control del drawdown y kill switch
    6. 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
  3. 4.3 · Microestructura y ejecución

    1. 4.3.1 Implementation shortfall, VWAP, TWAP y Almgren-Chriss
    2. 4.3.2 Order flow imbalance, señales de microestructura y latencia
  4. 4.4 · Brokers y APIs

    1. 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
    2. 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
    4. 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
    5. 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
  5. 4.5 · Sistema de trading en vivo

    1. 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
    2. 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
    3. 4.5.3 Paper trading, shadow trading y seguimiento del slippage
    4. 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
    5. 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
    6. 4.5.6 NautilusTrader o LEAN en real: del backtest a producción con el mismo código

Los pesos se mueven solos

Las lecciones anteriores del módulo calculan pesos: de Markowitz (4.1.1), con covarianzas mejor estimadas (4.1.2), con visiones (4.1.3), por riesgo (4.1.4), con restricciones de coste (4.1.5). Todos son pesos de un día. En cuanto los precios se mueven, el peso de cada activo cambia sin que nadie opere:

wi,t+1=wi,t 1+ri,t+11+rp,t+1,rp,t+1=∑jwj,t rj,t+1w_{i,t+1} = w_{i,t}\,\frac{1 + r_{i,t+1}}{1 + r_{p,t+1}}, \qquad r_{p,t+1} = \sum_j w_{j,t}\, r_{j,t+1}

Lo que sube gana peso y lo que baja lo pierde. Una cartera que no se toca acaba siendo, con los años, una apuesta por lo que más ha subido. Rebalancear es devolver los pesos a su objetivo, y la pregunta práctica tiene tres partes: cuándo (por calendario o cuando la desviación pasa de un umbral), hasta dónde (al objetivo o al borde de la banda) y a qué coste.

calendariofuera de bandanoPrecios del díaPesos derivados¿Toca operar?Operar al objetivoEsperarCostesPesos nuevos
Fuente Mermaid
flowchart TB
  P[Precios del día] --> D[Pesos derivados]
  D --> C{¿Toca operar?}
  C -- calendario --> O[Operar al objetivo]
  C -- fuera de banda --> O
  C -- no --> E[Esperar]
  O --> K[Costes]
  K --> N[Pesos nuevos]

La cartera de la lección es una cartera clásica de cinco ETF del universo del nivel (README de los datos): 40 % de renta variable de EE. UU. (RV_EEUU), 10 % de emergentes (RV_EMG), 30 % de bonos del Tesoro a 7-10 años (RF_T710), 10 % de crédito con grado de inversión (RF_CIG) y 10 % de oro (MP_ORO). Los datos son sintéticos, de 2006 a 2025, con las crisis del guion (2008, 2020, 2022) y una liquidez por activo que incluye la horquilla en calma y en estrés:

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

precios = pd.read_parquet("universo4_precios.parquet").set_index(
    "fecha")
fac = pd.read_parquet("universo4_factores_diarios.parquet").set_index(
    "fecha")
liq = pd.read_csv("riesgo4_liquidez.csv", index_col="activo")
OBJ = pd.Series({"RV_EEUU": 0.40, "RV_EMG": 0.10, "RF_T710": 0.30,
                 "RF_CIG": 0.10, "MP_ORO": 0.10})
A = list(OBJ.index)
# rendimiento simple total (con el tipo libre); base 100 la víspera
R = precios[A] / precios[A].shift(1).fillna(100) - 1
rf = fac["rf"]
crisis = fac["regimen"].ne("calma").to_numpy()
crece = (1 + R).prod()
final = OBJ * crece / (OBJ * crece).sum()
pd.DataFrame({"objetivo": OBJ, "sin_tocar_2025": final,
              "media_horquilla_pb": liq.loc[A, "horquilla_pb"] / 2,
              "en_estres_pb": liq.loc[A, "horquilla_pb_estres"] / 2}
             ).round(3)
OUT [01]La cartera objetivo y sus rendimientos
         objetivo  sin_tocar_2025  media_horquilla_pb  en_estres_pb
RV_EEUU       0.4           0.554               0.435         1.745
RV_EMG        0.1           0.052               0.485         1.950
RF_T710       0.3           0.117               1.390        11.120
RF_CIG        0.1           0.054               1.355        10.835
MP_ORO        0.1           0.222               2.715        32.575

Sin tocarla en 20 años, la cartera terminaría con un 55,4 % en la bolsa de EE. UU. (frente al 40 % pedido), un 22,2 % en oro (frente al 10 %) y solo un 11,7 % en el bono del Tesoro (frente al 30 %). Ya no sería la cartera que se eligió: tendría más del doble de oro y casi un tercio de los bonos. La horquilla, en cambio, es pequeña: media horquilla de 0,435 pb en el ETF de la bolsa de EE. UU. y 2,7 pb en el de oro en calma. En los regímenes de estrés del guion se multiplica por 4 en la renta variable, por 8 en los bonos y por 12 en el oro (de 2,7 a 32,6 pb): rebalancear en plena crisis es más caro, y es justo cuando más se desvían los pesos.

Un simulador con las dos familias de reglas

El simulador sigue la cartera día a día en dólares: aplica los rendimientos, calcula los pesos y, si la regla lo pide, opera al cierre hasta el objetivo. Cada orden paga media horquilla sobre su importe (la de estrés fuera de los tramos de calma) y, si se indica, una comisión fija por orden. Las reglas de calendario operan el último día de cada periodo pase lo que pase; las de banda miran cada cierre y operan solo si algún activo se ha salido de su banda, que puede ser absoluta (5 puntos: un 40 % puede ir de 35 % a 45 %) o relativa (20 % del peso: un 10 % puede ir de 8 % a 12 %):

IN [02]python
def cortes(fechas, freq):
    """True en la última sesión de cada periodo (M, Q o Y)."""
    p = fechas.to_period(freq)
    return np.r_[p[1:] != p[:-1], False]

def rebalanceo(regla, capital=1e6, fijo=0.0, banda=0.05,
               relativa=False):
    """Cartera con pesos objetivo OBJ, decisiones al cierre.
    regla: 'diario', 'mensual', 'trimestral', 'anual', 'nunca' o
    'banda' (se mira cada cierre). Coste: media horquilla (la de
    estrés fuera de la calma) sobre el efectivo + `fijo` USD por
    orden. Devuelve el rendimiento diario y un resumen."""
    w = OBJ.to_numpy()
    hs = liq.loc[A, "horquilla_pb"].to_numpy() / 2e4
    he = liq.loc[A, "horquilla_pb_estres"].to_numpy() / 2e4
    toca = {"diario": np.ones(len(R), bool),
            "mensual": cortes(R.index, "M"),
            "trimestral": cortes(R.index, "Q"),
            "anual": cortes(R.index, "Y"),
            "nunca": np.zeros(len(R), bool)}.get(regla)
    lim = banda * w if relativa else np.full(len(w), banda)
    x = capital * w
    rend, peso_rv = np.empty(len(R)), np.empty(len(R))
    ordenes = giro = coste = desv = 0.0
    for t, r in enumerate(R.to_numpy()):
        v0 = x.sum()
        x = x * (1 + r)
        v = x.sum()
        peso = x / v
        desv = max(desv, np.abs(peso - w).max())
        hacer = toca[t] if toca is not None else (
            np.abs(peso - w) > lim).any()
        if hacer:
            orden = w * v - x
            n = (np.abs(orden) > 0.01).sum()
            c = (np.abs(orden) * (he if crisis[t] else hs)).sum()
            c += fijo * n
            ordenes += n
            giro += np.abs(orden).sum() / v / 2
            coste += c / v
            x = w * (v - c)
        rend[t] = x.sum() / v0 - 1
        peso_rv[t] = x[:2].sum() / x.sum()
    años = len(R) / 252
    exc = pd.Series(rend, R.index) - rf
    capital_t = (1 + pd.Series(rend, R.index)).cumprod()
    return exc, pd.Series(peso_rv, R.index), {
        "ordenes_año": ordenes / años,
        "giro_pct_año": 100 * giro / años,
        "coste_pb_año": 1e4 * coste / años,
        "exceso_pct": 100 * exc.mean() * 252,
        "vol_pct": 100 * exc.std() * np.sqrt(252),
        "sharpe": exc.mean() / exc.std() * np.sqrt(252),
        "caida_max_pct": 100 * (capital_t / capital_t.cummax()
                                - 1).min(),
        "desv_max_pp": 100 * desv}

Tres detalles del código que importan en un sistema real. El rendimiento de la cartera se calcula en dinero, con rendimientos simples, no con una media ponderada de rendimientos logarítmicos (que infravalora el Sharpe, como se vio en 3.1.4). La rotación (giro) cuenta la mitad del importe negociado, la convención habitual: vender un 5 % de un activo y comprar un 5 % de otro es una rotación del 5 %. Y el coste se descuenta del valor antes de fijar los pesos nuevos, así que una cartera que rebalancea mucho tiene algo menos de capital invertido que otra que no lo hace.

Calendario frente a bandas

La referencia ideal es la cartera con los pesos exactos todos los días y sin costes, R @ OBJ: lo que se llama a veces la cartera «de pesos constantes». Cada regla se compara con ella por el error de seguimiento (la volatilidad anual de la diferencia de rendimientos):

IN [03]python
REGLAS = {"diario": {"regla": "diario"},
          "mensual": {"regla": "mensual"},
          "trimestral": {"regla": "trimestral"},
          "anual": {"regla": "anual"},
          "nunca": {"regla": "nunca"},
          "banda 5 pp": {"regla": "banda", "banda": 0.05},
          "banda 20 % rel.": {"regla": "banda", "banda": 0.20,
                              "relativa": True}}
ideal = R @ OBJ - rf          # pesos exactos cada día, sin costes
res, rend, pesos = {}, {}, {}
for nombre, kw in REGLAS.items():
    exc, peso_rv, m = rebalanceo(**kw)
    m["error_seg_pct"] = 100 * (exc - ideal).std() * np.sqrt(252)
    res[nombre], rend[nombre], pesos[nombre] = m, exc, peso_rv
tabla = pd.DataFrame(res).T
tabla.columns = ["ordenes", "giro_%", "coste_pb", "exceso_%", "vol_%",
                 "sharpe", "caida_%", "desv_pp", "error_%"]
tabla.drop(columns=["exceso_%", "vol_%"]).round(2)
OUT [03]Siete reglas con un millón de USD
                 ordenes  giro_%  coste_pb  sharpe  caida_%  desv_pp  error_%
diario           1260.00   66.92      4.21    0.49   -33.39     2.40     0.01
mensual            59.86   16.65      1.00    0.51   -33.14     8.26     0.54
trimestral         19.79    9.98      0.59    0.52   -32.08     9.26     0.81
anual               4.76    5.38      0.31    0.53   -29.94    12.10     1.10
nunca               0.00    0.00      0.00    0.55   -29.64    20.33     2.31
banda 5 pp          5.76    8.06      0.81    0.56   -31.24     6.55     0.79
banda 20 % rel.     6.76    7.36      0.66    0.53   -32.44     8.39     0.69

Con un millón de USD en ETF muy líquidos, el coste no decide nada: rebalancear cada día, con 1.260 órdenes y una rotación del 67 % anual, cuesta 4,2 pb al año; cada mes, 1,0 pb; una banda de 5 puntos, 0,8 pb. Lo que separa a las reglas es el control de los pesos:

  • Por calendario, cuanto más espaciado, menos rotación y más desviación: la desviación máxima de un activo pasa de 2,4 puntos (diario) a 8,3 (mensual), 9,3 (trimestral) y 12,1 (anual), y el error de seguimiento de 0,01 % a 1,10 %. Sin rebalancear, la desviación llega a 20,3 puntos y el error a 2,31 %.
  • Por bandas, la desviación queda acotada por construcción (6,55 puntos como máximo con la banda de 5: el exceso sobre 5 es el movimiento del último día) con muy pocas órdenes: 5,8 al año, menos que el rebalanceo anual. La banda de 5 puntos tiene un error de seguimiento (0,79 %) como el trimestral (0,81 %) con menos de un tercio de órdenes. La banda relativa del 20 % protege más los pesos pequeños (un 10 % no puede pasar del 12 %) y menos los grandes.

La banda gana porque solo opera cuando la desviación importa: en años tranquilos casi no toca la cartera y en una crisis opera varias veces seguidas. El calendario opera igual en los dos casos.

Cuando la comisión es fija

Con una cuenta pequeña el problema cambia de naturaleza. Muchos brókeres minoristas cobran una comisión mínima o fija por orden, y entonces el coste depende del número de órdenes y no de su tamaño. El plan fijo del bróker ficticio Brio de los datos del nivel 2 (costes2_brokers.csv, lección 2.4.7) cobra 4,95 USD por orden:

IN [04]python
brokers = pd.read_csv("costes2_brokers.csv")
fijo = float(brokers.loc[brokers["plan"] == "fijo", "tarifa"].iloc[0])
pequena = {}
for nombre in ["mensual", "trimestral", "anual", "banda 5 pp",
               "banda 20 % rel."]:
    exc, _, m = rebalanceo(capital=25_000, fijo=fijo, **REGLAS[nombre])
    pequena[nombre] = {k: m[k] for k in ["ordenes_año",
                                         "coste_pb_año", "sharpe"]}
print(f"comisión fija: {fijo} USD por orden")
pd.DataFrame(pequena).T.round(2)
OUT [04]Las comisiones fijas con una cuenta pequeña
comisión fija: 4.95 USD por orden
                 ordenes_año  coste_pb_año  sharpe
mensual                59.86         76.44    0.44
trimestral             19.79         24.21    0.50
anual                   4.76          5.75    0.53
banda 5 pp              5.76          7.19    0.55
banda 20 % rel.         6.76          8.83    0.52
Fig. — Coste anual de rebalancear una cartera de 25.000 USD y cinco ETF con una comisión fija de 4,95 USD por orden más la media horquilla, según la regla (2006-2025)

Con 25.000 USD, el rebalanceo mensual cuesta 76 pb al año y baja el Sharpe de 0,51 a 0,44; el trimestral, 24 pb. La banda de 5 puntos cuesta 7,2 pb con prácticamente el mismo Sharpe que sin costes (0,55 frente a 0,56). El rebalanceo diario ni se incluye: 1.260 órdenes al año a 4,95 USD son 6.237 USD, una cuarta parte de la cuenta. El orden de magnitud es lo que cuenta: con comisiones fijas, la frecuencia de rebalanceo tiene que bajar hasta que el número de órdenes deje de pesar, y una banda es la forma natural de conseguirlo sin dejar los pesos a su suerte.

Fuera de la simulación hay un coste más que suele pesar más que todos estos: los impuestos. Vender lo que ha subido para rebalancear realiza una ganancia, y en una cuenta sujeta a impuestos eso adelanta el pago. Las reglas fiscales dependen del país, del vehículo (fondo, ETF, cuenta de pensiones) y cambian con el tiempo: hay que consultarlas en la fuente oficial antes de elegir la regla. Una forma de reducir ventas es rebalancear con el dinero que entra (aportaciones y dividendos) hacia lo que está por debajo de su peso.

El riesgo deriva aunque la rentabilidad no lo diga

La figura de la cabecera dibuja el peso de la renta variable (bolsa de EE. UU. más emergentes, objetivo 50 %) sin rebalancear y con la banda de 5 puntos. A fin de año:

IN [05]python
rv = pd.DataFrame(pesos)[["nunca", "anual", "banda 5 pp"]]
fin_año = rv.groupby(rv.index.year).last()
print(fin_año.loc[[2007, 2008, 2013, 2019, 2021, 2022, 2025]]
      .mul(100).round(1))
OUT [05]El peso de la renta variable sin rebalancear
       nunca  anual  banda 5 pp
fecha
2007    55.1   50.0        48.4
2008    38.8   50.0        47.6
2013    36.5   50.0        49.0
2019    52.6   50.0        49.8
2021    56.2   50.0        54.5
2022    53.0   50.0        46.5
2025    60.7   48.9        50.2

Sin tocarla, la cartera pasó de un 55,1 % de renta variable a finales de 2007 a un 38,8 % un año después, seguía en un 36,5 % en 2013 y terminó en un 60,7 % en 2025; el mínimo mensual fue un 32,6 % en febrero de 2009 y el máximo, un 65,2 % en noviembre de 2024. Es decir, la cartera sin rebalancear tuvo menos renta variable justo después del desplome, cuando estaba barata, y más que nunca tras quince años de subidas. La regla anual vuelve al 50 % cada diciembre (en 2025 marca 48,9 % porque el último día de la muestra no se rebalancea) y la banda se mueve entre el 41 % y el 56 %.

¿Qué hacen las reglas en una crisis? Rebalancear obliga a comprar lo que cae:

IN [06]python
TRAMOS = {"2008-09 a 2009-03": ("2008-09-02", "2009-03-09"),
          "rebote 2009": ("2009-03-10", "2009-12-31"),
          "2020-02 a 03": ("2020-02-20", "2020-03-23"),
          "2022": ("2022-01-04", "2022-10-12")}
total = {k: v + rf for k, v in rend.items()}
pd.DataFrame({t: {k: 100 * ((1 + s.loc[a:b]).prod() - 1)
                  for k, s in total.items()}
              for t, (a, b) in TRAMOS.items()}).round(1)
OUT [06]Las reglas en las crisis
                 2008-09 a 2009-03  rebote 2009  2020-02 a 03  2022
diario                       -25.5         30.1         -18.7 -20.8
mensual                      -25.5         30.7         -17.7 -20.8
trimestral                   -24.6         30.6         -17.9 -20.6
anual                        -21.9         29.4         -17.9 -20.5
nunca                        -21.0         24.3         -18.9 -19.9
banda 5 pp                   -23.9         31.3         -18.2 -20.4
banda 20 % rel.              -24.8         31.4         -18.4 -20.9

En la caída de septiembre de 2008 a marzo de 2009, la cartera rebalanceada a diario pierde un 25,5 % y la que no se toca, un 21,0 %: la segunda llega con menos renta variable (la caída ya le había quitado peso) y no compra en la bajada. En el rebote de 2009 pasa lo contrario: 30,1 % frente a 24,3 %. Es el coste y el peor episodio de la regla: rebalancear empeora la caída de una crisis larga y mejora la recuperación; en una caída corta y seguida de rebote (2020) apenas cambia nada (de −17,7 % a −18,9 %), y en 2022, con acciones y bonos cayendo a la vez, tampoco (de −19,9 % a −20,9 %): cuando todo cae, no hay nada que vender caro para comprar barato.

Lo que no se puede concluir: la rentabilidad

La tabla anterior da a «nunca» el mejor Sharpe por calendario (0,55). Antes de concluir nada, la diferencia frente al mensual por quinquenios, y una comparación con la teoría:

IN [07]python
dif = pd.DataFrame({k: rend[k] - rend["mensual"]
                    for k in ["nunca", "anual", "banda 5 pp"]})
quinq = dif.groupby((dif.index.year - 2006) // 5).mean() * 252e4
quinq.index = ["2006-10", "2011-15", "2016-20", "2021-25"]
print(quinq.round(0))
años = len(R) / 252
crec = np.log1p(R).sum() / años              # crecimiento log anual
diario = np.log1p(R @ OBJ).sum() / años
nunca = np.log((OBJ * (1 + R).prod()).sum()) / años
bono = 0.5 * (OBJ @ R.var() - OBJ @ R.cov() @ OBJ) * 252
print(f"media ponderada de los activos {100 * (OBJ @ crec):.2f} %, "
      f"diario {100 * diario:.2f} %, nunca {100 * nunca:.2f} %")
print(f"½(Σ w·σ² − σ²_cartera) = {1e4 * bono:.0f} pb al año")
OUT [07]Diferencia de rentabilidad por quinquenios
         nunca  anual  banda 5 pp
2006-10   39.0   99.0        90.0
2011-15 -114.0  -37.0        12.0
2016-20 -165.0  -17.0        70.0
2021-25  248.0   18.0        28.0
media ponderada de los activos 5.75 %, diario 6.54 %, nunca 6.74 %
½(Σ w·σ² − σ²_cartera) = 79 pb al año

No rebalancear rinde 39 pb al año más que el mensual en 2006-2010, 114 y 165 menos en los dos quinquenios siguientes y 248 más en el último. El signo cambia con quién gana en cada tramo. No hay una regla que rinda más de forma fiable; hay reglas que controlan mejor el riesgo.

La segunda parte del resultado aclara un argumento que circula mucho, el «bono del rebalanceo». Booth y Fama (1992) mostraron que el crecimiento compuesto de una cartera de pesos constantes supera a la media ponderada del crecimiento de sus activos en aproximadamente 12(∑iwiσi2−σp2)\tfrac12(\sum_i w_i\sigma_i^2 - \sigma_p^2): el rendimiento de diversificación. En los datos, la media ponderada del crecimiento logarítmico de los cinco activos es un 5,75 % anual y la cartera rebalanceada cada día crece un 6,54 %: 79 pb de diferencia, exactamente lo que da la fórmula. Pero la cartera que no se rebalancea crece un 6,74 %, todavía más. No hay contradicción: la cartera sin tocar no crece como la media ponderada de sus activos de partida, sino que se va cargando de los que más suben (aquí el oro, con un 10,7 % anual de crecimiento, y la bolsa de EE. UU., con un 8,4 %). Si los ganadores hubieran sido otros, el resultado sería el contrario. El rendimiento de diversificación existe, pero no es una ventaja garantizada frente a no hacer nada.

Una cartera de estrategias

El proyecto del nivel combina estrategias de los niveles 2 y 3, no activos. Una estrategia se puede tratar como un activo más (su serie de rendimientos), con tres diferencias. Primera: sus rendimientos ya están dimensionados por la propia estrategia (cada una con su volatilidad objetivo), así que el «peso» es la fracción del capital que se le asigna. Segunda: rebalancear entre estrategias mueve capital entre sistemas que a su vez operan; el coste depende de lo que negocia cada una. Tercera: las estrategias se degradan, y una cartera que asigna por el pasado sigue dando capital a una estrategia que ha dejado de funcionar (lo trata 4.2.5).

Nueve estrategiasVol. y covarianzaPesos a fin de mesCartera con derivaVol. objetivoKill switch 4.2.5
Fuente Mermaid
flowchart TB
  S[Nueve estrategias] --> V[Vol. y covarianza]
  V --> W[Pesos a fin de mes]
  W --> C[Cartera con deriva]
  C --> T[Vol. objetivo]
  C --> K[Kill switch 4.2.5]

Los datos del nivel traen nueve estrategias parecidas a las de los niveles 2 y 3 (más una que deja de funcionar en 2019, degradada, que se deja para el segundo ejercicio), con rendimientos diarios simples en exceso del tipo libre de 2006 a 2025 y eventos fechados: el crash de los quants de agosto de 2007 en pares, el desplome del carry en octubre de 2008, el crash de momentum de 2009, el Volmageddon de febrero de 2018. La correlación verdadera es baja, y lo que interesa es si se mantiene en los malos momentos:

IN [08]python
est = pd.read_parquet("estrategias4_diarias.parquet").set_index("fecha")
E = est.drop(columns="degradada")
regimen = fac["regimen"]
rho = {g: E[regimen == g].corr() for g in ["calma", "estres"]}
iu = np.triu_indices(E.shape[1], 1)
for g, c in rho.items():
    print(f"{g:7s} correlación media {c.to_numpy()[iu].mean():.3f}")
par = ["venta_puts", "venta_volatilidad", "carry_divisas"]
print(rho["calma"].loc[par, par].round(2))
print(rho["estres"].loc[par, par].round(2))
OUT [08]Nueve estrategias y sus correlaciones
calma   correlación media 0.082
estres  correlación media 0.084
                   venta_puts  venta_volatilidad  carry_divisas
venta_puts               1.00               0.57           0.33
venta_volatilidad        0.57               1.00           0.27
carry_divisas            0.33               0.27           1.00
                   venta_puts  venta_volatilidad  carry_divisas
venta_puts               1.00               0.61           0.56
venta_volatilidad        0.61               1.00           0.42
carry_divisas            0.56               0.42           1.00

La correlación media de las 36 parejas apenas se mueve entre la calma (0,082) y los regímenes de estrés del universo (0,084). La media engaña: entre las tres estrategias que «venden seguros» (venta de puts, venta de volatilidad, carry de divisas), la correlación sube en estrés (de 0,33 a 0,56 entre puts y carry de divisas; de 0,27 a 0,42 entre volatilidad y carry), mientras otras parejas, con la tendencia, bajan. Las pérdidas se concentran.

Los pesos se calculan el último día de cada mes con datos hasta ese día y se aplican desde la sesión siguiente. Tres asignaciones: equiponderada, inversa a la volatilidad de 63 sesiones (la risk parity ingenua, sin correlaciones) y mínima varianza con solo largos y la covarianza de Ledoit-Wolf de un año (lección 4.1.2), optimizada con cvxpy. La risk parity completa, HRP y HERC son de la lección 4.1.4:

IN [09]python
import cvxpy as cp
from sklearn.covariance import LedoitWolf

def minima_varianza(S):
    w = cp.Variable(len(S))
    cp.Problem(cp.Minimize(cp.quad_form(w, cp.psd_wrap(S))),
               [cp.sum(w) == 1, w >= 0]).solve(solver="CLARABEL")
    return np.clip(w.value, 0, None) / np.clip(w.value, 0, None).sum()

f = E.index
fin_mes = np.flatnonzero(cortes(f, "M"))
PESOS = {"equiponderada": {}, "inversa_vol": {}, "min_varianza": {}}
for i in fin_mes[fin_mes >= 251]:
    iv = 1 / E.iloc[i - 62:i + 1].std()
    S = LedoitWolf().fit(E.iloc[i - 251:i + 1].to_numpy()).covariance_
    d = f[i + 1]                   # desde la sesión siguiente
    PESOS["equiponderada"][d] = np.full(E.shape[1], 1 / E.shape[1])
    PESOS["inversa_vol"][d] = (iv / iv.sum()).to_numpy()
    PESOS["min_varianza"][d] = minima_varianza(S)
ult = pd.DataFrame({k: v[max(v)] for k, v in PESOS.items()},
                   index=E.columns)
print("pesos desde", max(PESOS["inversa_vol"]).date())
ult.round(3)
OUT [09]Pesos a fin de mes sin mirar el futuro
pesos desde 2025-12-01
                   equiponderada  inversa_vol  min_varianza
tendencia_futuros          0.111        0.086         0.169
carry_futuros              0.111        0.157         0.190
carry_divisas              0.111        0.121         0.071
venta_puts                 0.111        0.112         0.040
pares                      0.111        0.120         0.169
momentum_acciones          0.111        0.066         0.049
valor_acciones             0.111        0.076         0.109
venta_volatilidad          0.111        0.067         0.000
reversion_corto            0.111        0.195         0.201

Los últimos pesos (desde el 1 de diciembre de 2025) muestran la diferencia de criterio. La inversa de la volatilidad da más capital a las estrategias tranquilas (reversión a corto plazo, 19,5 %; carry de futuros, 15,7 %) y menos a las volátiles (momentum, 6,6 %; venta de volatilidad, 6,7 %). La mínima varianza, que además ve las correlaciones, deja la venta de volatilidad a cero y reduce la venta de puts al 4 %: las dos se mueven juntas, y con una basta. Sube la tendencia (16,9 %), que se correlaciona negativamente con casi todas.

IN [10]python
def cartera(nuevos, datos=E, coste_pb=2.0):
    """Rendimiento diario con pesos que derivan y se rehacen en las
    fechas de `nuevos`; coste por unidad de peso negociado."""
    w, rs, idx, giro = None, [], [], 0.0
    for d, r in zip(datos.index, datos.to_numpy()):
        c = 0.0
        if d in nuevos:
            obj = nuevos[d]
            if w is not None:
                giro += np.abs(obj - w).sum()
                c = np.abs(obj - w).sum() * coste_pb / 1e4
            w = obj
        if w is None:
            continue
        rs.append(w @ r - c)
        idx.append(d)
        w = w * (1 + r) / (w @ (1 + r))
    s = pd.Series(rs, idx)
    return s, giro / (len(s) / 252)

def resumen(s, giro=np.nan):
    eq = (1 + s).cumprod()
    dd = eq / eq.cummax() - 1
    m = {"sharpe": s.mean() / s.std() * np.sqrt(252),
         "vol_pct": 100 * s.std() * np.sqrt(252),
         "exceso_pct": 100 * s.mean() * 252,
         "caida_max_pct": 100 * dd.min(),
         "giro_pct_año": 100 * giro}
    return {**{k: round(v, 2) for k, v in m.items()},
            "fecha_peor": dd.idxmin().date()}

CARTERAS = {k: cartera(v) for k, v in PESOS.items()}
pd.DataFrame({k: resumen(*v) for k, v in CARTERAS.items()}).T
OUT [10]Las tres asignaciones, con deriva entre fines de mes
              sharpe vol_pct exceso_pct caida_max_pct giro_pct_año  fecha_peor
equiponderada   0.84    4.47       3.77        -14.54        24.68  2009-05-05
inversa_vol      1.0    3.75       3.74        -10.48        142.7  2009-02-20
min_varianza    0.94    3.49       3.29         -8.58       111.26  2008-05-13

La deriva entre fines de mes se calcula con los rendimientos en exceso de cada estrategia (todas cobran el mismo tipo libre sobre el capital, así que la aproximación es buena), y cada rebalanceo paga 2 pb por unidad de peso negociado, un supuesto de la lección. De 2007 a 2025, la equiponderada da un Sharpe de 0,84 con un 4,5 % de volatilidad, la inversa a la volatilidad 1,00 con un 3,75 % y la mínima varianza 0,94 con un 3,5 %. Las caídas máximas son −14,5 %, −10,5 % y −8,6 %. Ninguna de estas carteras rinde mucho por sí sola (un 3,3 % a 3,8 % anual en exceso): con correlaciones tan bajas su volatilidad es pequeña, y la forma de llevarla a un riesgo útil es apalancarla hasta una volatilidad objetivo, que es la lección 4.2.2.

El precio de las carteras que miran la volatilidad es la rotación: 143 % al año la inversa a la volatilidad y 111 % la mínima varianza, frente al 25 % de la equiponderada (que solo deshace la deriva). Y cabe preguntarse si conviene recalcular más a menudo:

IN [11]python
diario = {}
vol = E.rolling(63).std()
for i in range(fin_mes[fin_mes >= 251][0], len(f) - 1):
    iv = 1 / vol.iloc[i]
    diario[f[i + 1]] = (iv / iv.sum()).to_numpy()
s_d, giro_d = cartera(diario)
pd.DataFrame({"diaria": resumen(s_d, giro_d),
              "mensual": resumen(*CARTERAS["inversa_vol"])}).T
OUT [11]Rehacer los pesos cada día o cada mes
        sharpe vol_pct exceso_pct caida_max_pct giro_pct_año  fecha_peor
diaria    1.04    3.66       3.82         -10.1       359.95  2009-02-20
mensual    1.0    3.75       3.74        -10.48        142.7  2009-02-20

Con los pesos rehechos cada día, el Sharpe pasa de 1,00 a 1,04 (el README de los datos mide 1,10 con pesos diarios sin costes y desde 2006) y la rotación de 143 % a 360 % al año. La mejora no viene de «rebalancear más», sino de que la volatilidad de cada estrategia se agrupa en el tiempo: la inversa a la volatilidad recalculada a diario recorta antes la estrategia que acaba de entrar en un tramo agitado, una forma de volatility targeting estrategia a estrategia. Con 2 pb por unidad de peso compensa; con estrategias caras de mover (opciones, crédito), podría no hacerlo.

El peor tramo de la cartera

IN [12]python
s = CARTERAS["inversa_vol"][0]
eq = (1 + s).cumprod()
dd = eq / eq.cummax() - 1
pico = eq.loc[:dd.idxmin()].idxmax()
print(f"de {pico.date()} a {dd.idxmin().date()}: "
      f"{100 * dd.min():.1f} %")
tramo = E.loc[pico:dd.idxmin()]
print((100 * ((1 + tramo).prod() - 1)).sort_values().round(1))
print(s.nsmallest(3).mul(100).round(2))
OUT [12]El peor tramo de la cartera de estrategias
de 2007-04-02 a 2009-02-20: -10.5 %
venta_puts          -45.0
venta_volatilidad   -37.2
carry_divisas       -22.1
reversion_corto     -21.0
carry_futuros       -15.6
pares               -13.2
valor_acciones       -4.9
momentum_acciones     4.7
tendencia_futuros    44.9
dtype: float64
2018-02-05   -1.35
2008-09-25   -1.02
2018-10-03   -0.94
dtype: float64
Fig. — Peor tramo de la cartera de nueve estrategias con pesos inversos a la volatilidad (2007-04-02 a 2009-02-20): variación de cada estrategia y de la cartera

La cartera inversa a la volatilidad cae un 10,5 % desde el 2 de abril de 2007 hasta el 20 de febrero de 2009. En ese tramo, la venta de puts pierde un 45,0 %, la venta de volatilidad un 37,2 % y el carry de divisas un 22,1 %; la tendencia gana un 44,9 % y compensa una parte. El peor día es el 5 de febrero de 2018 (−1,35 %), el del Volmageddon de los datos. Una cartera de estrategias «poco correladas» no está diversificada contra lo que de verdad la amenaza: que varias estrategias sean, en el fondo, la misma apuesta (vender seguros contra caídas), con sus pérdidas en los mismos días. La lección 4.2.4 trata esos escenarios.

Las mismas reglas en bt

bt (la librería de backtesting de carteras de la lección 2.4.6) trae las dos familias de reglas como bloques: los de calendario (RunMonthly, RunQuarterly) y RunIfOutOfBounds, que en la versión 1.4.1 dispara cuando algún peso se desvía de su objetivo más que una tolerancia relativa (0,2 = un 20 % del peso objetivo, la banda relativa de la lección). bt no está en el entorno del curso (como en 2.4.6, su código va sin ejecutar), y el de abajo está comprobado contra el código fuente de esa versión:

IN [13]rebalanceo_bt.pypython
import bt

precios = ...   # DataFrame de precios, una columna por activo
pesos = {"RV_EEUU": 0.40, "RV_EMG": 0.10, "RF_T710": 0.30,
         "RF_CIG": 0.10, "MP_ORO": 0.10}
trimestral = bt.Strategy("trimestral", [
    bt.algos.RunQuarterly(),
    bt.algos.SelectAll(),
    bt.algos.WeighSpecified(**pesos),
    bt.algos.Rebalance()])
bandas = bt.Strategy("bandas_20_rel", [
    bt.algos.SelectAll(),
    bt.algos.WeighSpecified(**pesos),
    bt.algos.Or([bt.algos.RunOnce(),
                 bt.algos.RunIfOutOfBounds(0.20)]),
    bt.algos.Rebalance()])
comision = lambda cantidad, precio: 4.95      # fija por orden
res = bt.run(bt.Backtest(trimestral, precios, commissions=comision,
                         initial_capital=25_000),
             bt.Backtest(bandas, precios, commissions=comision,
                         initial_capital=25_000))
res.display()

El orden de la pila importa: RunIfOutOfBounds necesita los pesos objetivo en temp["weights"], así que va después de WeighSpecified, y RunOnce compra la cartera el primer día. Backtest usa por defecto posiciones enteras (integer_positions=True), lo que con 25.000 USD añade un pequeño error de redondeo que el simulador de la lección no tiene.

Errores típicos

  • Comparar reglas por su rentabilidad en un backtest. La diferencia depende de qué ganó en la muestra; compara órdenes, costes, desviación y error de seguimiento.
  • Olvidar las comisiones fijas o mínimas. Con cuentas pequeñas deciden la frecuencia.
  • Recalcular pesos con datos del día en que se aplican. Los pesos de fin de mes se aplican desde la sesión siguiente.
  • Promediar rendimientos logarítmicos para la cartera, o rebalancear «sin deriva» (pesos fijos cada día) en un backtest que dice rebalancear cada mes.
  • Fiarse de la correlación media. Mira qué estrategias pierden juntas en los peores tramos.

Ejercicios

  1. Con la cuenta de 25.000 USD y la comisión fija, compara bandas de 2,5, 5 y 10 puntos: órdenes, coste, desviación máxima y error de seguimiento.
Solución
IN [14]python
filas = {}
for b in (0.025, 0.05, 0.10):
    exc, _, m = rebalanceo("banda", capital=25_000, fijo=fijo, banda=b)
    filas[f"{100 * b:g} pp"] = {
        "ordenes_año": m["ordenes_año"],
        "coste_pb_año": m["coste_pb_año"],
        "desv_max_pp": m["desv_max_pp"],
        "error_seg_pct": 100 * (exc - ideal).std() * np.sqrt(252)}
pd.DataFrame(filas).T.round(2)
OUT [14]Bandas más anchas y más estrechas
        ordenes_año  coste_pb_año  desv_max_pp  error_seg_pct
2.5 pp        17.53         22.51         4.34           0.32
5 pp           5.76          7.19         6.55           0.78
10 pp          1.50          2.13        10.52           1.47

Al estrechar la banda a 2,5 puntos las órdenes se triplican (17,5 al año) y el coste también (22,5 pb), con un error de seguimiento de 0,32 %; al ensancharla a 10 puntos bastan 1,5 órdenes al año (2,1 pb), pero la desviación máxima llega a 10,5 puntos y el error a 1,47 %, peor que el rebalanceo anual. La anchura se elige como un compromiso entre coste y control: con esta cuenta, 5 puntos es un punto razonable.

  1. Añade la estrategia degradada a la cartera inversa a la volatilidad y compara el Sharpe antes y después del 1 de julio de 2019 con la cartera de nueve. ¿Cuánto capital recibe después de dejar de funcionar?
Solución
IN [15]python
PESOS10 = {}
for i in fin_mes[fin_mes >= 251]:
    iv = 1 / est.iloc[i - 62:i + 1].std()
    PESOS10[f[i + 1]] = (iv / iv.sum()).to_numpy()
s10, _ = cartera(PESOS10, datos=est)
peso_deg = pd.Series({d: w[-1] for d, w in PESOS10.items()})
s9 = CARTERAS["inversa_vol"][0]
for a, b in [("2007", "2019-06"), ("2019-07", "2025")]:
    x10, x9 = s10.loc[a:b], s9.loc[a:b]
    print(f"{a} a {b}: con degradada "
          f"{x10.mean() / x10.std() * np.sqrt(252):.2f}, sin ella "
          f"{x9.mean() / x9.std() * np.sqrt(252):.2f}, peso medio "
          f"{100 * peso_deg.loc[a:b].mean():.1f} %")
OUT [15]La estrategia degradada dentro de la cartera
2007 a 2019-06: con degradada 1.10, sin ella 0.74, peso medio 11.6 %
2019-07 a 2025: con degradada 1.37, sin ella 1.50, peso medio 10.7 %

Hasta junio de 2019, la estrategia degradada (Sharpe 1,30 en ese tramo según el README) sube el Sharpe de la cartera de 0,74 a 1,10. Después, con media cero, lo baja de 1,50 a 1,37, y la cartera le sigue dando un 10,7 % del capital de media: la inversa a la volatilidad no mira la rentabilidad, solo el riesgo. Asignar por Sharpe pasado tampoco lo arregla bien (un Sharpe de 3 años tiene un error típico cercano a 0,6). Detectar que una estrategia ha dejado de funcionar y retirarla con reglas fijadas de antemano es el tema de la lección 4.2.5.

  1. Un compañero propone rebalancear la cartera de cinco ETF «solo cuando el mercado ha caído un 20 %», para comprar barato. ¿Qué tiene de banda y qué no? ¿Qué pasaría en 2022?
Solución

Tiene de banda que solo opera cuando algo se ha movido mucho, pero mira el mercado y no los pesos: una caída del 20 % de la bolsa con los bonos subiendo (2008) desvía mucho la cartera y la regla opera; una subida larga de la bolsa (2013-2021) también desvía la cartera, hacia arriba, y la regla no la corrige nunca, así que el riesgo crece sin control. En 2022 la bolsa cayó un 26 % y los bonos un 15 % a la vez (README de los datos): los pesos se desviaron poco y la regla operaría vendiendo bonos que también habían caído, sin «comprar barato» nada. La banda sobre los pesos resuelve las dos cosas, porque opera cuando la cartera ya no es la elegida, en cualquier dirección.

Respuesta plegada en cada pregunta

Autoevaluación

  1. Una cartera 50/50 de acciones y bonos no se toca durante un año en el que las acciones suben un 30 % y los bonos un 0 %. ¿Qué peso tienen las acciones al final?

    • A50 %
    • BUn 65 %
    • CDepende de la volatilidad
    • DUn 56,5 %: 0,5·1,3 / (0,5·1,3 + 0,5)
    Ver respuesta

    Respuesta D. Un 56,5 %: 0,5·1,3 / (0,5·1,3 + 0,5)

    Cada peso se multiplica por (1 + su rendimiento) y se divide por (1 + el de la cartera): 0,65 / 1,15 = 0,565. La deriva no depende de la volatilidad sino de los rendimientos realizados.

  2. Con 25.000 USD y una comisión fija de 4,95 USD por orden, el rebalanceo mensual de cinco ETF cuesta 76 pb al año en la lección. ¿Qué lo explica?

    • AEl número de órdenes: unas 60 al año, casi todas de importes pequeños, cada una con su comisión fija
    • BLa horquilla de los ETF
    • CEl impacto en el mercado
    • DLos impuestos
    Ver respuesta

    Respuesta A. El número de órdenes: unas 60 al año, casi todas de importes pequeños, cada una con su comisión fija

    Rebalancear cada mes manda órdenes por desviaciones mínimas. Con una comisión fija, el coste depende del número de órdenes y no de su tamaño: 60 × 4,95 USD son 297 USD al año, un 1,2 % de 25.000 USD en los primeros años y algo menos según crece la cuenta. Con una banda de 5 puntos bastan unas 6 órdenes al año.

  3. En la muestra, no rebalancear nunca da un Sharpe de 0,55 y rebalancear cada mes, 0,51. ¿Qué conclusión es correcta?

    • AQue no hay que rebalancear
    • BQue el rebalanceo mensual es demasiado caro
    • CNinguna sobre la rentabilidad: la diferencia cambia de signo por quinquenios (+39, −114, −165 y +248 pb al año) y depende de qué activos ganaron; lo que sí es robusto es que sin rebalancear el peso de la renta variable vagó entre el 33 % y el 65 %
    • DQue el Sharpe está mal calculado
    Ver respuesta

    Respuesta C. Ninguna sobre la rentabilidad: la diferencia cambia de signo por quinquenios (+39, −114, −165 y +248 pb al año) y depende de qué activos ganaron; lo que sí es robusto es que sin rebalancear el peso de la renta variable vagó entre el 33 % y el 65 %

    La cartera sin tocar se carga de lo que más sube. En estos 20 años ganaron el oro y la bolsa de EE. UU., y por eso le fue algo mejor; con otro reparto de ganadores habría salido al revés. El riesgo, en cambio, deriva siempre: es la razón de fondo para rebalancear.

  4. ¿Por qué la cartera de estrategias con pesos inversos a la volatilidad recalculados cada día (Sharpe 1,04) mejora a la misma cartera recalculada cada mes (1,00)?

    • APorque rebalancear cada día siempre es mejor
    • BPorque la volatilidad de cada estrategia se agrupa en el tiempo: recalcular a diario recorta antes la estrategia cuya volatilidad acaba de subir, que es una forma de volatility targeting por estrategia, a cambio de una rotación de 360 % al año frente a 143 %
    • CPor los costes
    • DPor azar: la diferencia es enorme
    Ver respuesta

    Respuesta B. Porque la volatilidad de cada estrategia se agrupa en el tiempo: recalcular a diario recorta antes la estrategia cuya volatilidad acaba de subir, que es una forma de volatility targeting por estrategia, a cambio de una rotación de 360 % al año frente a 143 %

    Los rendimientos de las estrategias tienen GARCH: los días de volatilidad alta vienen juntos. Pesos que se adaptan rápido reducen la exposición en esos tramos. La ganancia (0,04 de Sharpe) es pequeña y cuesta rotación; con costes reales por estrategia altos, podría no compensar.

  5. La correlación media entre las nueve estrategias es 0,082 en calma y 0,084 en estrés. ¿Significa que la diversificación aguanta en las crisis?

    • ANo: la media esconde que las estrategias que venden seguros (venta de puts, de volatilidad, carry de divisas) se correlacionan más entre sí en estrés (0,33 a 0,56 entre puts y carry de divisas) mientras la tendencia se mueve al revés; las pérdidas se concentran en el mismo grupo
    • BSí, la correlación no cambia
    • CNo, porque en estrés todas las correlaciones son 1
    • DSí, porque el Sharpe es alto
    Ver respuesta

    Respuesta A. No: la media esconde que las estrategias que venden seguros (venta de puts, de volatilidad, carry de divisas) se correlacionan más entre sí en estrés (0,33 a 0,56 entre puts y carry de divisas) mientras la tendencia se mueve al revés; las pérdidas se concentran en el mismo grupo

    Una media de 36 correlaciones puede quedarse quieta mientras unas suben y otras bajan. Lo que importa es quién pierde a la vez: en el peor tramo, venta de puts −45 %, venta de volatilidad −37 % y carry de divisas −22 %, compensados en parte por la tendencia (+45 %).

Ideas clave

  • Sin rebalancear, cada peso se multiplica por (1 + su rendimiento) / (1 + el de la cartera): en 2006-2025 la renta variable de la cartera de la lección (objetivo 50 %) fue del 33 % al 65 % y el oro del 10 % al 22 %.
  • Con un millón de USD en ETF líquidos el coste de rebalancear es de 1 a 4 pb al año en cualquier regla; con 25.000 USD y comisión fija por orden manda el número de órdenes: 76 pb al año el mensual frente a 7 pb una banda de 5 puntos.
  • Las bandas consiguen con 6 órdenes al año un error de seguimiento (0,79 %) como el del rebalanceo trimestral con 20, porque solo operan cuando la desviación importa.
  • La diferencia de rentabilidad entre reglas es ruido de la trayectoria (cambia de signo por quinquenios); lo robusto es el control del riesgo. El rendimiento de diversificación medido, 79 pb al año, coincide con ½(Σw·σ² − σ²) de la teoría.
  • Una cartera de nueve estrategias poco correladas (0,08 de media) da un Sharpe de 0,84 equiponderada, 1,00 con pesos inversos a la volatilidad y 0,94 con mínima varianza, todos estimados sin anticipación; su peor tramo, −10,5 %, llega cuando las tres estrategias que venden seguros pierden a la vez.

Recursos