Lección 2.4.7 · 70 min

Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos

Un backtest sin costes mide una estrategia que nadie puede operar. Esta lección modela cada coste por separado (las tarifas de tres brokers ficticios, la horquilla, el impacto de mercado, la financiación del apalancamiento y el préstamo de las acciones vendidas en corto) y los aplica a una estrategia long-short de momentum del universo del nivel 2.

Fig. — Comisión de una orden en puntos básicos del efectivo según su importe, con los cuatro planes de los brokers ficticios del nivel 2 y una acción de 50 USD
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

Tres familias de costes

La lección 1.1.5 midió los costes de una orden concreta sobre el libro de NOVA. Aquí el problema es otro: meterlos en un backtest de años, con miles de órdenes, de forma que el resultado neto se parezca al que daría una cuenta real. Para eso hay que separar los costes por la variable de la que dependen, porque cada uno se modela distinto.

Coste totalExplícitosImplícitosMantenimientoComisión y tasasMedia horquillaImpactoFinanc. y préstamo
Fuente Mermaid
flowchart TB
  C[Coste total] --> E[Explícitos]
  C --> I[Implícitos]
  C --> M[Mantenimiento]
  E --> CO[Comisión y tasas]
  I --> HS[Media horquilla]
  I --> IM[Impacto]
  M --> FI[Financ. y préstamo]
CosteDepende deSe conoce…Cómo se modela aquí
ComisiónAcciones, efectivo, órdenes y volumen del mesAntes de operar (tarifa)Función por plan, con tests
Media horquillaLiquidez del título y del momentoAproximadamente (datos)horquilla_pb de cada acción y mes
ImpactoTamaño frente al volumen y volatilidadSolo estimadoLey de la raíz cuadrada, con supuestos
FinanciaciónApalancamiento y tipo de interésAntes (contrato del broker)Tipo libre más un diferencial supuesto
Préstamo de títulosQué acción se vende en corto y cuándoDía a día (mercado de préstamo)costes2_prestamo.csv: tasa y disponibilidad

Los datos son del nivel 2 del curso: sintéticos, con tarifas y tasas inventadas pero con la forma de las reales. Los nombres de los brokers (Alcor, Brio, Cenit) son ficticios.

Comisiones: cuatro esquemas, cuatro funciones

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

tarifas = pd.read_csv("costes2_brokers.csv")
tarifas.drop(columns=["tipo", "unidad"])
OUT [01]Las tarifas de los tres brokers
  broker        plan  tramo_desde  tramo_hasta  tarifa  minimo_orden  maximo_pct_efectivo
0  Alcor  por_accion            0          NaN  0.0050          1.00                  1.0
1  Alcor  escalonado            0     300000.0  0.0035          0.35                  1.0
2  Alcor  escalonado       300000    3000000.0  0.0020          0.35                  1.0
3  Alcor  escalonado      3000000   20000000.0  0.0015          0.35                  1.0
4  Alcor  escalonado     20000000          NaN  0.0010          0.35                  1.0
5   Brio        fijo            0          NaN  4.9500           NaN                  NaN
6  Cenit  porcentaje            0          NaN  0.0800          2.00                  NaN

Cuatro esquemas que se encuentran en brokers reales:

  • Por acción con mínimo y máximo (Alcor, plan por_accion): 0,005 USD por acción, al menos 1 USD por orden y como mucho el 1 % del efectivo.
  • Escalonado por volumen mensual (Alcor, plan escalonado): la tarifa por acción baja a medida que crece el volumen negociado en el mes; el mínimo baja a 0,35 USD.
  • Fijo por orden (Brio): 4,95 USD por orden, sea de 10 acciones o de 10.000.
  • Porcentaje del efectivo (Cenit): el 0,08 % del importe, con un mínimo de 2 USD.

Una sola función recibe las filas del plan, el número de acciones, el precio y las acciones ya negociadas en el mes (solo cuentan para el escalonado):

IN [02]python
def comision(plan, acciones, precio, volumen_mes=0):
    """Comisión en USD de una orden según las filas de un plan."""
    acciones = abs(acciones)
    efectivo = acciones * precio
    fila = plan.iloc[0]
    if fila["tipo"] == "escalonado":
        # tramo según el volumen YA negociado en el mes
        hasta = plan["tramo_hasta"].fillna(np.inf)
        fila = plan[(plan["tramo_desde"] <= volumen_mes)
                    & (volumen_mes < hasta)].iloc[0]
    if fila["tipo"] in ("por_accion", "escalonado"):
        c = fila["tarifa"] * acciones
    elif fila["tipo"] == "fijo_por_orden":
        c = fila["tarifa"]
    elif fila["tipo"] == "porcentaje":
        c = fila["tarifa"] / 100 * efectivo
    else:
        raise ValueError(f"tipo desconocido: {fila['tipo']}")
    if pd.notna(fila["minimo_orden"]):
        c = max(c, fila["minimo_orden"])
    if pd.notna(fila["maximo_pct_efectivo"]):
        c = min(c, fila["maximo_pct_efectivo"] / 100 * efectivo)
    return round(c, 4)


planes = {f"{b}/{p}": g.reset_index(drop=True)
          for (b, p), g in tarifas.groupby(["broker", "plan"])}
list(planes)
OUT [02]La comisión de una orden
['Alcor/escalonado', 'Alcor/por_accion', 'Brio/fijo', 'Cenit/porcentaje']

Una función de costes es código de producción: si cobra mal, todos los backtests que la usan están mal. Se prueba con casos calculados a mano, uno por regla: el mínimo, el máximo, cada tramo y los bordes entre tramos.

IN [03]python
def test_por_accion_normal():
    # 1.000 acciones a 50 USD: 5 USD, entre mínimo y máximo
    assert comision(planes["Alcor/por_accion"], 1000, 50) == 5.0

def test_por_accion_minimo():
    assert comision(planes["Alcor/por_accion"], 20, 50) == 1.0

def test_por_accion_maximo():
    # 1.000 acciones a 0,20 USD: 5 USD > 1 % de 200 USD
    assert comision(planes["Alcor/por_accion"], 1000, 0.2) == 2.0

def test_escalonado_tramos():
    p = planes["Alcor/escalonado"]
    assert comision(p, 1000, 50, volumen_mes=0) == 3.5
    assert comision(p, 1000, 50, volumen_mes=299_999) == 3.5
    assert comision(p, 1000, 50, volumen_mes=300_000) == 2.0
    assert comision(p, 1000, 50, volumen_mes=25_000_000) == 1.0

def test_escalonado_minimo():
    assert comision(planes["Alcor/escalonado"], 10, 50) == 0.35

def test_fijo_y_porcentaje():
    assert comision(planes["Brio/fijo"], 10, 50) == 4.95
    assert comision(planes["Brio/fijo"], 10_000, 50) == 4.95
    assert comision(planes["Cenit/porcentaje"], 1000, 50) == 40.0
    assert comision(planes["Cenit/porcentaje"], 10, 50) == 2.0

def test_venta_igual_que_compra():
    p = planes["Alcor/por_accion"]
    assert comision(p, -1000, 50) == comision(p, 1000, 50)

tests = [f for n, f in list(globals().items())
         if n.startswith("test_") and callable(f)]
for t in tests:
    t()
print(f"{len(tests)} tests correctos")
OUT [03]Tests de la función de comisión
7 tests correctos

En un proyecto, estas funciones van en un módulo y los tests en un fichero que ejecuta pytest (lección 0.4.5); aquí se llaman a mano para que la celda sea autocontenida. El test de la venta parece trivial, pero es el error más frecuente al reutilizar una función de comisión con cantidades con signo.

El coste según el tamaño de la orden

Con las funciones probadas, el coste de cada plan en puntos básicos del efectivo para órdenes de 500 USD a 1 millón de USD, con una acción de 50 USD:

IN [04]python
precio = 50.0
importes = [500, 1_000, 5_000, 20_000, 50_000,
            100_000, 500_000, 1_000_000]
curva = pd.DataFrame({
    nombre: [1e4 * comision(p, int(i / precio), precio) / i
             for i in importes]
    for nombre, p in planes.items()}, index=importes)
curva.index.name = "importe_usd"
curva.round(2)
OUT [04]Coste por tamaño de orden y broker
             Alcor/escalonado  Alcor/por_accion  Brio/fijo  Cenit/porcentaje
importe_usd
500                       7.0              20.0      99.00              40.0
1000                      3.5              10.0      49.50              20.0
5000                      0.7               2.0       9.90               8.0
20000                     0.7               1.0       2.48               8.0
50000                     0.7               1.0       0.99               8.0
100000                    0.7               1.0       0.50               8.0
500000                    0.7               1.0       0.10               8.0
1000000                   0.7               1.0       0.05               8.0

La figura de la cabecera dibuja estas curvas en escala logarítmica. Tres lecturas:

  • Los mínimos mandan en las órdenes pequeñas. Con 500 USD, el plan por acción de Alcor cuesta 20 pb y el fijo de Brio, 99 pb: casi un 1 % por lado. Un sistema que rota una cuenta pequeña en órdenes pequeñas paga más en comisiones de lo que suele ganar.
  • El fijo por orden gana en las órdenes grandes. Brio cobra lo mismo por 10.000 acciones que por 10; por encima de unos 50.000 USD (a 50 USD por acción) es más barato que el plan por acción.
  • El porcentaje no escala. Cenit cuesta 8 pb siempre que se supere su mínimo: para órdenes grandes es diez veces más caro que los otros.

El punto de cruce entre Brio y el plan por acción de Alcor depende del precio de la acción, porque una tarifa por acción pesa más, en proporción, en las acciones baratas:

IN [05]python
importe = 20_000
filas = []
for px in [5, 20, 50, 200]:
    n = int(importe / px)
    filas.append({
        "precio": px, "acciones": n,
        "alcor_pb": 1e4 * comision(planes["Alcor/por_accion"],
                                   n, px) / importe,
        "brio_pb": 1e4 * comision(planes["Brio/fijo"],
                                  n, px) / importe})
pd.DataFrame(filas).set_index("precio").round(2)
OUT [05]La tarifa por acción y el precio
        acciones  alcor_pb  brio_pb
precio
5           4000      10.0     2.48
20          1000       2.5     2.48
50           400       1.0     2.48
200          100       0.5     2.48

Una orden de 20.000 USD cuesta 10 pb en una acción de 5 USD y 0,5 pb en una de 200 (donde ya manda el mínimo de 1 USD). Un backtest que modela las comisiones como un porcentaje fijo del efectivo subestima el coste de las estrategias que compran acciones baratas, que suelen ser las pequeñas, las caídas y las que están cerca de quebrar: exactamente las que pueblan la pata corta de un momentum o la larga de un value.

Horquilla e impacto: la ley de la raíz cuadrada

Una orden a mercado paga la media horquilla: compra al ask o vende al bid, la mitad de la horquilla lejos del punto medio. En el universo del nivel 2, universo2_caracteristicas.parquet trae para cada acción y mes la horquilla media cotizada, horquilla_pb, que el generador hace depender de la capitalización y de la volatilidad, y que se multiplica por 1,6 en los tramos de estrés:

IN [06]python
car = pd.read_parquet("universo2_caracteristicas.parquet")
car["tamano"] = car.groupby("fecha")["capitalizacion"].transform(
    lambda c: pd.qcut(c, 3, labels=["pequeña", "mediana", "grande"]))
calma, estres = pd.to_datetime(["2007-06-29", "2008-10-31"])
tabla = (car[car["fecha"].isin([calma, estres])]
         .pivot_table(index="tamano", columns="fecha",
                      values="horquilla_pb", aggfunc="median",
                      observed=True))
tabla.columns = ["junio 2007", "octubre 2008"]
tabla.round(1)
OUT [06]La horquilla por tamaño y en estrés
         junio 2007  octubre 2008
tamano
pequeña         5.1          12.8
mediana         3.5           8.0
grande          2.4           5.0

La horquilla mediana de las pequeñas es el doble de la de las grandes, y en octubre de 2008 todas se abren entre dos y tres veces. Lo mismo pasa en los datos reales: los costes suben justo cuando las estrategias más operan (las de tendencia reaccionan a las caídas, las de reversión compran en ellas), y un coste medio de toda la muestra no lo recoge.

La horquilla cubre órdenes pequeñas. Una orden que es una fracción apreciable del volumen negociado mueve el precio mientras se ejecuta: es el impacto de mercado. La regularidad empírica más robusta sobre su tamaño es la ley de la raíz cuadrada: el impacto medio de una orden grande (una «metaorden» repartida en el día) crece como la raíz cuadrada de su tamaño relativo al volumen, escalado por la volatilidad diaria1Nota 1 · Tóth, Lempérière, Deremble, de Lataillade, Kockelkoren y Bouchaud (2011), «Anomalous price impact and the critical nature of liquidity in financial markets», Physical Review X 1, 021006, proponen una explicación de la ley y aportan evidencia empírica; el libro de Bouchaud, Bonart, Donier y Gould (2018) la desarrolla. Los dos están en los recursos.:

ΔPP≈Y σdQV\frac{\Delta P}{P} \approx Y \,\sigma_{d} \sqrt{\frac{Q}{V}}

con QQ las acciones de la orden, VV el volumen diario medio y σd\sigma_d la volatilidad diaria. El coeficiente YY no es una constante universal: se estima con datos de ejecuciones propios y cambia con el mercado y con la forma de ejecutar. En esta lección Y=0,7Y = 0{,}7 es un supuesto, y la volatilidad es la desviación típica de los rendimientos diarios de los últimos 63 días. Para tres acciones del universo a 30 de junio de 2015, la mayor, la mediana y la menor por capitalización:

IN [07]python
aj = pd.read_parquet("universo2_ajustados.parquet")
px = aj.pivot(index="fecha", columns="ticker",
              values="cierre_ajustado")
sigma = px.pct_change(fill_method=None).rolling(
    63, min_periods=40).std()

fecha = pd.Timestamp("2015-06-30")
dia = car[car["fecha"] == fecha].sort_values("capitalizacion")
tres = dia.iloc[[-1, len(dia) // 2, 0]][
    ["ticker", "cierre", "capitalizacion", "volumen_medio_21d",
     "horquilla_pb"]].set_index("ticker")
tres["sigma_d_pct"] = 100 * sigma.loc[fecha, tres.index]
Y = 0.7

def coste_total_pb(t, importe):
    fila = tres.loc[t]
    acciones = int(importe / fila["cierre"])
    com = comision(planes["Alcor/por_accion"], acciones,
                   fila["cierre"])
    media_horq = fila["horquilla_pb"] / 2
    part = acciones / fila["volumen_medio_21d"]
    impacto = 1e4 * Y * fila["sigma_d_pct"] / 100 * np.sqrt(part)
    return 1e4 * com / importe + media_horq + impacto

for imp in [10_000, 1_000_000, 10_000_000]:
    tres[f"{imp:,}"] = [coste_total_pb(t, imp) for t in tres.index]
tres.drop(columns="capitalizacion").round(2)
OUT [07]Tres acciones y su coste por tamaño
        cierre  volumen_medio_21d  horquilla_pb  sigma_d_pct  10,000  1,000,000  10,000,000
ticker
QXEH    236.94           16348165           1.8         1.88    2.11       3.22        7.79
QXNR     79.11             590137           3.7         1.43    4.31      17.10       48.71
QXWM     12.56             155718          23.9         2.31   27.50     131.64      381.83
Fig. — Coste de un lado (comisión, media horquilla e impacto con la ley de la raíz cuadrada) según el importe, para tres acciones del universo a 30 de junio de 2015

La tabla y la figura muestran el coste total de un lado (comisión, media horquilla e impacto) para tres tamaños. Con 10.000 USD casi todo es horquilla, y la pequeña cuesta ya más de diez veces lo que la grande (27,5 pb frente a 2,1). Con 10 millones, el impacto lo domina: en la grande, la orden es una fracción minúscula de lo que se negocia al día y cuesta 7,8 pb; en la pequeña es más de cinco veces su volumen diario, y el coste estimado, 382 pb, ya no tiene sentido como coste de una sola orden: habría que repartirla en muchos días. Por eso la capacidad de una estrategia (lección 2.2.3) la fija casi siempre su pata menos líquida, y por eso un backtest con un coste fijo por operación sobrevalora las estrategias que operan acciones pequeñas con mucho capital.

Financiación y préstamo de títulos en un long-short

Una cartera long-short compra unas acciones y vende en corto otras. Vender en corto exige tomar prestadas las acciones: el prestamista cobra una tasa anual sobre el valor prestado, y el efectivo de la venta queda como colateral, que el broker puede remunerar (el rebate). Si la cartera usa apalancamiento, la parte larga que no cubre el capital propio se financia con un crédito a un tipo mayor que el libre de riesgo. costes2_prestamo.csv da, por acción y tramo de fechas, la tasa de préstamo y la disponibilidad:

IN [08]python
prestamo = pd.read_csv("costes2_prestamo.csv",
                       parse_dates=["desde", "hasta"])
print(prestamo.groupby("disponibilidad")["tasa_prestamo_anual"]
      .describe()[["count", "min", "50%", "max"]])
prestamo[prestamo["disponibilidad"] == "nula"]
OUT [08]Préstamo de títulos: tasas y disponibilidad
                count    min    50%   max
disponibilidad
alta            129.0   0.25   0.38   0.5
baja             10.0   6.30  20.60  24.4
nula              4.0  26.40  38.45  52.8
    ticker      desde      hasta  tasa_prestamo_anual disponibilidad
51    QXKV 2008-04-27 2009-05-29                 52.8           nula
90    QXQT 2007-09-16 2008-10-17                 26.4           nula
132   QXYX 2019-06-02 2020-07-02                 31.9           nula
142   QXZT 2015-04-12 2016-05-13                 45.0           nula

La tasa general está entre el 0,25 % y el 0,50 % anual. Las acciones difíciles de tomar prestadas cuestan del 6 % al 24 %, y las cuatro con disponibilidad nula no se pueden vender en corto a ningún precio durante su último año. Son las cuatro quiebras del universo (universo2_eventos.csv). En el mercado real pasa lo mismo: las acciones que más caen son las más demandadas para vender en corto, y el préstamo se encarece o desaparece.

La estrategia de prueba es la de la lección 2.3.5: momentum 12-1 en todo el universo que cotiza cada mes, comprado el quintil ganador y vendido el perdedor, con pesos iguales, 100 % largo y 100 % corto, rebalanceo mensual. En estos datos el momentum tiene una prima plantada (README del nivel 2: diferencia entre quintiles de ≈ 8 % anual) con dos crashes en 2009 y 2020. Primero, las rentabilidades mensuales con la de deslistado incluida:

IN [09]python
eventos = pd.read_csv("universo2_eventos.csv",
                      parse_dates=["fecha"])
mensual = px.resample("ME").last()
r = mensual.pct_change(fill_method=None)
for _, e in eventos.dropna(
        subset=["rentabilidad_deslistado"]).iterrows():
    mes = px[e["ticker"]].last_valid_index() + pd.offsets.MonthEnd(0)
    r.loc[mes, e["ticker"]] = ((1 + r.loc[mes, e["ticker"]])
                               * (1 + e["rentabilidad_deslistado"]) - 1)

# momentum 12-1 al cierre de cada mes: meses m-11 a m-1
mom = (1 + r).rolling(11).apply(np.prod, raw=True).shift(1) - 1
car["mes"] = car["fecha"] + pd.offsets.MonthEnd(0)
def mensual_de(col):
    return car.pivot(index="mes", columns="ticker",
                     values=col).reindex(r.index)
precio_m = mensual_de("cierre")
adv_m = mensual_de("volumen_medio_21d")
horq_m = mensual_de("horquilla_pb")
sigma_m = sigma.resample("ME").last()
tipo = pd.read_csv("tipo_libre2.csv", parse_dates=["fecha"])
rf_m = (tipo.set_index("fecha")["tipo_3m"].resample("ME").mean()
        .reindex(r.index) / 100)
print(r.loc["2008-10-31", ["QXQT"]].round(3).to_dict())
OUT [09]Rentabilidad mensual con la de deslistado
{'QXQT': -1.0}

Sin esa corrección, la quiebra de QXQT en octubre de 2008 desaparecería de los datos con su última cotización de 0,85 USD, y el −100 % no lo cobraría nadie. Con ella, el mes de la quiebra cuenta entero.

El backtest aplica, en cada rebalanceo, todos los costes de esta lección. Los supuestos van en la cabecera de la función, a la vista:

SupuestoValor
Capital20 millones de USD
ComisiónAlcor, plan por acción
Horquillahorquilla_pb del mes, media horquilla por lado
ImpactoY=0,7Y = 0{,}7, σd\sigma_d de 63 días, volumen de 21 días
Préstamotasa de costes2_prestamo.csv (0,25 % si falta)
Rebate del colateraltipo libre − 0,5 %
Crédito del apalancamientotipo libre + 1 %
IN [10]python
K, REBATE, CREDITO = 20e6, -0.005, 0.01


def condiciones_prestamo(fecha):
    vigente = prestamo[(prestamo["desde"] <= fecha)
                       & (prestamo["hasta"] >= fecha)]
    v = vigente.set_index("ticker")
    return v["tasa_prestamo_anual"] / 100, v["disponibilidad"]


def long_short(excluir_nula=True, apal=1.0, capital=K):
    pesos, filas = pd.Series(dtype=float), []
    fwd = r.shift(-1)
    for f in r.index[12:-1]:
        m = mom.loc[f].dropna()
        m = m[precio_m.loc[f, m.index].notna()]
        q = pd.qcut(m.rank(method="first"), 5, labels=False)
        tasa, disp = condiciones_prestamo(f)
        largos = list(q.index[q == 4])
        cortos = [t for t in q.index[q == 0]
                  if not (excluir_nula and disp.get(t) == "nula")]
        obj = pd.concat([pd.Series(apal / len(largos), largos),
                         pd.Series(-apal / len(cortos), cortos)])
        giro = obj.sub(pesos, fill_value=0).abs()
        giro = giro[giro > 0]  # los deslistados ya pesan 0
        efectivo = capital * giro
        acc = efectivo / precio_m.loc[f, giro.index]
        part = acc / adv_m.loc[f, giro.index]
        com = sum(comision(planes["Alcor/por_accion"], a, p)
                  for a, p in zip(acc, precio_m.loc[f, giro.index]))
        horq = (giro * horq_m.loc[f, giro.index] / 2e4).sum()
        imp = (giro * Y * sigma_m.loc[f, giro.index]
               * np.sqrt(part)).sum()
        bruto = apal * (fwd.loc[f, largos].mean()
                        - fwd.loc[f, cortos].mean())
        filas.append({
            "fecha": f + pd.offsets.MonthEnd(1),
            "bruto": bruto,
            "comision": com / capital,
            "horquilla": horq,
            "impacto": imp,
            "prestamo": apal * tasa.reindex(cortos)
                        .fillna(0.0025).mean() / 12,
            "rebate": apal * (rf_m.loc[f] + REBATE) / 12,
            "credito": max(apal - 1, 0) * (rf_m.loc[f] + CREDITO) / 12,
            "rotacion": giro.sum() / 2})
        # los pesos derivan con las rentabilidades del mes
        rr = fwd.loc[f, obj.index].fillna(-1.0)
        pesos = obj * (1 + rr) / (1 + bruto)
    d = pd.DataFrame(filas).set_index("fecha")
    d["neto"] = (d["bruto"] - d["comision"] - d["horquilla"]
                 - d["impacto"] - d["prestamo"] + d["rebate"]
                 - d["credito"])
    return d


ingenuo = long_short(excluir_nula=False)
real = long_short()
print(len(real), "meses:", real.index[0].date(), "a",
      real.index[-1].date())
OUT [10]Backtest long-short con todos los costes
227 meses: 2007-02-28 a 2025-12-31

El peso de cada acción deriva durante el mes con su rentabilidad, así que cada rebalanceo paga solo la diferencia entre el peso que hay y el objetivo. Una acción que deja de cotizar llega al rebalanceo con peso cero (su fillna(-1) es «ya no está»), y la cartera la sustituye. La descomposición anual de las dos versiones:

IN [11]python
def anual(d):
    a = 12 * d.drop(columns="rotacion").mean()
    a["rotacion_anual"] = 12 * d["rotacion"].mean()
    a["sharpe_bruto"] = (d["bruto"].mean() / d["bruto"].std()
                         * np.sqrt(12))
    a["sharpe_neto"] = (d["neto"].mean() / d["neto"].std()
                        * np.sqrt(12))
    return a

comparativa = pd.DataFrame({"ingenuo": anual(ingenuo),
                            "realista": anual(real)})
comparativa.round(4)
OUT [11]Del bruto al neto
                ingenuo  realista
bruto            0.0971    0.0670
comision         0.0022    0.0022
horquilla        0.0044    0.0043
impacto          0.0206    0.0203
prestamo         0.0084    0.0044
rebate           0.0105    0.0105
credito          0.0000    0.0000
neto             0.0766    0.0506
rotacion_anual   6.2178    6.2478
sharpe_bruto     0.8059    0.5541
sharpe_neto      0.6337    0.4159
Fig. — Momentum 12-1 long-short en el universo del nivel 2, 2007-2025: bruto vendiendo en corto lo que no se puede tomar prestado, bruto realista y neto de todos los costes

Leída de arriba abajo, la tabla cuenta la historia completa:

  • El bruto ingenuo (9,7 % anual) vende en corto las quiebras durante su último año. La restricción de préstamo sola le quita tres puntos: el bruto realista es del 6,7 %, cerca del ≈ 8 % que el README declara para la prima plantada entre quintiles, menos los crashes.
  • El impacto (2,0 % anual) es el coste dominante: la cartera rota unas seis veces su valor al año, y con 20 millones de USD las órdenes en las acciones pequeñas son una fracción apreciable de su volumen. La media horquilla (0,4 %) y la comisión (0,2 %) son menores.
  • El préstamo cuesta un 0,4 % en la versión realista y el doble en la ingenua, que paga tasas del 26 % al 53 % (si pudiera) por las quiebras.
  • El rebate suma un 1,1 % anual: con el efectivo de las ventas como colateral remunerado al tipo libre menos medio punto. En años de tipos altos, una parte apreciable del resultado de un long-short es este interés, no la señal.

El Sharpe pasa de 0,81 (ingenuo, bruto) a 0,42 (realista, neto). La figura muestra cómo se separan las curvas: la distancia entre el ingenuo y el realista se abre casi entera en 2007-2009, con las dos primeras quiebras; la del bruto al neto crece de forma continua, porque los costes se pagan en cada rebalanceo. Un euro de 2007 acaba en 5,44 en el ingenuo, en 3,09 en el bruto realista y en 2,24 en el neto. Los datos son sintéticos y la prima está plantada: lo que se aprende no es cuánto gana el momentum, sino que una ventaja bruta de 0,8 de Sharpe puede quedarse en poco más de la mitad cuando se modela lo que cuesta operarla.

Señal del mesPesos objetivoDisponibilidadGiro de pesosComisiónMedia horquillaImpacto raízPréstamo y rebateRentabilidad neta
Fuente Mermaid
flowchart TB
  S[Señal del mes] --> P[Pesos objetivo]
  D[Disponibilidad] --> P
  P --> G[Giro de pesos]
  G --> CO[Comisión]
  G --> H[Media horquilla]
  G --> I[Impacto raíz]
  P --> F[Préstamo y rebate]
  CO --> N[Rentabilidad neta]
  H --> N
  I --> N
  F --> N

El diagrama resume el orden de las operaciones de la función: la disponibilidad entra antes de fijar los pesos (un título que no se puede vender no entra en la cartera), los costes de transacción dependen del giro y los de mantenimiento, de las posiciones.

Errores típicos al modelar costes

  • Un coste fijo en puntos básicos para todo. Ignora los mínimos, la dependencia del precio por acción, la horquilla de las pequeñas y el impacto, que crece con el capital.
  • Costes de toda la muestra en vez de los del momento. La horquilla y la volatilidad suben en las crisis; un coste medio infravalora justo los periodos en que la estrategia más opera.
  • Vender en corto lo que no se puede tomar prestado. Regala las ganancias de las quiebras, como se ha visto.
  • Olvidar el interés. El rebate y el crédito pueden ser del mismo orden que la ventaja; con tipos del 5 %, un long-short que «gana el 6 %» puede estar ganando sobre todo intereses.
  • Calibrar el impacto con el backtest. El coeficiente YY se mide con ejecuciones reales (o se toma de la literatura como orden de magnitud) y se varía para ver cuánto depende el resultado de él; ajustarlo hasta que la estrategia salga bien es sobreajuste.

Ejercicios

  1. Encuentra el importe a partir del cual Brio (fijo por orden) es más barato que el plan por acción de Alcor para una acción de 20 USD y para una de 100 USD. Explica por qué cambia.
Solución
IN [12]python
for px_ in [20, 100]:
    for n in range(1, 10_000):
        if (comision(planes["Brio/fijo"], n, px_)
                < comision(planes["Alcor/por_accion"], n, px_)):
            print(f"precio {px_}: desde {n} acciones",
                  f"({n * px_:,} USD)")
            break
OUT [12]Punto de cruce por precio
precio 20: desde 991 acciones (19,820 USD)
precio 100: desde 991 acciones (99,100 USD)

El cruce está siempre en el mismo número de acciones, 991, donde 0,005 × n supera 4,95 USD; en efectivo, cinco veces más para una acción de 100 USD que para una de 20. La tarifa por acción es un coste por acción: para comparar brokers hay que pensar en acciones por orden, no en importes.

  1. Repite el backtest realista con 200 millones de USD de capital. ¿Qué costes cambian, cuánto y por qué?
Solución
IN [13]python
grande = long_short(capital=200e6)
pd.DataFrame({"20 M": anual(real), "200 M": anual(grande)}).loc[
    ["bruto", "comision", "horquilla", "impacto", "neto",
     "sharpe_neto"]].round(4)
OUT [13]Diez veces más capital
               20 M   200 M
bruto        0.0670  0.0670
comision     0.0022  0.0022
horquilla    0.0043  0.0043
impacto      0.0203  0.0642
neto         0.0506  0.0065
sharpe_neto  0.4159  0.0533

Solo cambia el impacto, que se multiplica por √10 ≈ 3,16 (del 2,0 % al 6,4 % anual): cada orden es diez veces mayor frente al mismo volumen. La comisión y la horquilla, en proporción, son las mismas (con 20 millones casi ninguna orden toca ya el mínimo de 1 USD). El neto cae del 5,1 % al 0,7 % y el Sharpe neto, de 0,42 a 0,05: es la capacidad de la estrategia (lección 2.2.3) medida con el modelo de costes. Con los supuestos de aquí, a partir de un capital así la estrategia deja de merecer la pena; con otro YY el número sería otro, y por eso hay que variarlo.

  1. Un compañero presenta un long-short con un Sharpe de 1,2 en 2023-2025 y dice que «el préstamo y la financiación son detalles». ¿Qué tres comprobaciones le pedirías antes de creérselo?
Solución

Primera: qué tipo de interés supone para el colateral de las ventas y para el crédito, y cuánto del resultado es ese interés (con tipos libres del 4-5 %, un rebate razonable aporta varios puntos al año que no tienen nada que ver con la señal). Segunda: si comprobó la disponibilidad de préstamo de cada título de la pata corta en cada fecha, y qué pasa con el resultado si se quitan los que eran difíciles de tomar prestados (en esta lección, ese cambio quitó tres puntos de bruto). Tercera: con qué capital y qué modelo de impacto está calculado, y cuánto cae el neto si el coeficiente del impacto se duplica; un Sharpe que solo sobrevive con impacto cero no es una estrategia.

Footnotes

  1. Tóth, Lempérière, Deremble, de Lataillade, Kockelkoren y Bouchaud (2011), «Anomalous price impact and the critical nature of liquidity in financial markets», Physical Review X 1, 021006, proponen una explicación de la ley y aportan evidencia empírica; el libro de Bouchaud, Bonart, Donier y Gould (2018) la desarrolla. Los dos están en los recursos. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. El plan por acción de Alcor cobra 0,005 USD por acción con un mínimo de 1 USD. ¿Cuánto cuesta, en puntos básicos, comprar 1.000 USD de una acción de 50 USD?

    • A1 punto básico
    • B10 puntos básicos, porque manda el mínimo
    • C0,5 puntos básicos
    • D50 puntos básicos
    Ver respuesta

    Respuesta B. 10 puntos básicos, porque manda el mínimo

    Son 20 acciones: 0,10 USD por acción, por debajo del mínimo de 1 USD. Un dólar sobre 1.000 son 10 pb. Con órdenes grandes el mismo plan cuesta 1 pb (0,005 / 50); con órdenes pequeñas, el mínimo multiplica el coste.

  2. La ley de la raíz cuadrada dice que el impacto de una orden de Q acciones es aproximadamente Y·σ·√(Q/V). Si una orden pasa del 1 % al 4 % del volumen diario, ¿cuánto cambia su impacto por acción?

    • ASe multiplica por 4
    • BSe multiplica por 2
    • CNo cambia
    • DSe multiplica por 16
    Ver respuesta

    Respuesta B. Se multiplica por 2

    √4 = 2. El impacto por acción crece con la raíz del tamaño, así que el coste total en dólares (impacto por acción por número de acciones) crece con la potencia 3/2: cuadruplicar la orden multiplica el coste total por 8.

  3. En el backtest long-short de momentum, el bruto pasa del 9,7 % al 6,7 % anual al quitar de la pata corta los títulos con disponibilidad «nula». ¿Por qué?

    • APorque se pierden posiciones largas
    • BPorque las acciones sin disponibilidad son las que van a quebrar: el backtest ingenuo gana vendiéndolas en corto, algo que en la realidad no se puede hacer
    • CPorque cambia el coste de la horquilla
    • DPor un error de redondeo
    Ver respuesta

    Respuesta B. Porque las acciones sin disponibilidad son las que van a quebrar: el backtest ingenuo gana vendiéndolas en corto, algo que en la realidad no se puede hacer

    Las cuatro quiebras del universo caen durante meses antes de deslistarse y entran en el quintil perdedor. Son justo las que el mercado de préstamo no ofrece (disponibilidad nula, tasas del 26 % al 53 %). Venderlas en corto en el backtest es una ganancia imposible.

  4. ¿Qué coste domina la estrategia long-short de la lección con 20 millones de USD?

    • ALa comisión
    • BEl impacto de mercado
    • CLa financiación
    • DLa horquilla
    Ver respuesta

    Respuesta B. El impacto de mercado

    Con una rotación de unas seis veces el capital al año, el impacto estimado (2,0 % anual) es casi cinco veces la media horquilla (0,4 %) y nueve veces la comisión (0,2 %). El impacto crece con el tamaño de la cartera; la comisión y la horquilla, en proporción, no.

  5. Un esquema de comisiones escalonado por volumen mensual cobra menos por acción a medida que el mes avanza. ¿Qué error típico introduce en un backtest usar siempre la tarifa del tramo más barato?

    • ANinguno, si el volumen total del año llega a ese tramo
    • BInfravalora el coste de las primeras órdenes de cada mes y, con poco volumen, de todas: el tramo depende del volumen acumulado en el mes, que se reinicia cada mes
    • CSobrevalora el coste
    • DCambia la horquilla
    Ver respuesta

    Respuesta B. Infravalora el coste de las primeras órdenes de cada mes y, con poco volumen, de todas: el tramo depende del volumen acumulado en el mes, que se reinicia cada mes

    La tarifa de cada orden depende del volumen ya negociado en el mes. Una cartera que mueve 200.000 acciones al mes no sale nunca del primer tramo de Alcor (0,0035 USD); suponer el más barato (0,001) divide su comisión por 3,5.

Ideas clave

  • Los costes son de tres tipos: explícitos (comisión, tasas), implícitos (horquilla, deslizamiento, impacto) y de mantenimiento (financiación, préstamo de títulos). Cada uno depende de una variable distinta.
  • Las comisiones tienen forma: mínimos que castigan las órdenes pequeñas, máximos, tramos por volumen mensual y tarifas por acción que pesan más en las acciones baratas. Se programan como funciones y se prueban.
  • El impacto crece con la raíz cuadrada de la participación en el volumen; su coeficiente es un supuesto que hay que declarar y variar.
  • En un long-short, la pata corta paga el préstamo de cada título y solo se puede vender lo que está disponible; ignorarlo regala justo las ganancias de las quiebras.
  • En la estrategia de la lección, los costes y las restricciones de préstamo bajan el Sharpe de 0,81 a 0,42: casi la mitad de lo que parecía ventaja.

Recursos