---
title: "2.4.7 Modelado de costes en un backtest"
description: "Comisiones por broker con mínimos, máximos y tramos, horquilla, impacto de raíz cuadrada, financiación y préstamo de títulos en un long-short."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-costes/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/modelado-de-costes.png"
---

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

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.4 · Backtesting: motores y paradigmas
- Duración: 70 min
- Requisitos: [1.1.5 · Costes, liquidación, apalancamiento y margen](https://pythonparatrading.com/curso-python-trading/nivel-1/costes-liquidacion-apalancamiento-y-margen/), [1.4.4 · Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo](https://pythonparatrading.com/curso-python-trading/nivel-1/sesgo-de-supervivencia-y-point-in-time/), [0.4.5 · Logging profesional y testing con pytest](https://pythonparatrading.com/curso-python-trading/nivel-0/logging-y-testing-con-pytest/)

## Al terminar esta lección

- Programar y probar las funciones de comisión de cuatro esquemas reales de tarifa: por acción con mínimo y máximo, escalonado por volumen mensual, fijo por orden y porcentaje del efectivo.
- Calcular el coste por tamaño de orden de cada broker y encontrar los puntos de cruce.
- Estimar el coste implícito de una operación con la media horquilla y la ley de la raíz cuadrada del impacto, con sus parámetros declarados como supuestos.
- Incluir en un backtest long-short la financiación del apalancamiento, la remuneración del colateral, el coste de préstamo y la imposibilidad de vender en corto los títulos sin disponibilidad.
- Descomponer la diferencia entre la rentabilidad bruta y la neta y explicar cuál de los costes manda.

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

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

| Coste | Depende de | Se conoce… | Cómo se modela aquí |
|---|---|---|---|
| Comisión | Acciones, efectivo, órdenes y volumen del mes | Antes de operar (tarifa) | Función por plan, con tests |
| Media horquilla | Liquidez del título y del momento | Aproximadamente (datos) | `horquilla_pb` de cada acción y mes |
| Impacto | Tamaño frente al volumen y volatilidad | Solo estimado | Ley de la raíz cuadrada, con supuestos |
| Financiación | Apalancamiento y tipo de interés | Antes (contrato del broker) | Tipo libre más un diferencial supuesto |
| Préstamo de títulos | Qué acción se vende en corto y cuándo | Dí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

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

tarifas = pd.read_csv("costes2_brokers.csv")
tarifas.drop(columns=["tipo", "unidad"])
```
```output
  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):

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

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

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

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

```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)
```
```output
         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 diaria[^1]:

$$
\frac{\Delta P}{P} \approx Y \,\sigma_{d} \sqrt{\frac{Q}{V}}
$$

con $Q$ las acciones de la orden, $V$ el volumen diario medio y $\sigma_d$ la volatilidad diaria. El coeficiente $Y$
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{,}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:

[^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.

```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)
```
```output
        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.* Tres curvas en escalas logarítmicas de 10.000 USD a 20 millones. La acción grande pasa de 2,1 pb a unos 10; la mediana, de 4,3 a unos 68; la pequeña (destacada) de 27,5 pb a 382 con 10 millones, cuando la orden es más de cinco veces su volumen diario. Con supuesto Y = 0,7.

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

```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"]
```
```output
                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:

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

| Supuesto | Valor |
|---|---|
| Capital | 20 millones de USD |
| Comisión | `Alcor`, plan por acción |
| Horquilla | `horquilla_pb` del mes, media horquilla por lado |
| Impacto | $Y = 0{,}7$, $\sigma_d$ de 63 días, volumen de 21 días |
| Préstamo | tasa de `costes2_prestamo.csv` (0,25 % si falta) |
| Rebate del colateral | tipo libre − 0,5 % |
| Crédito del apalancamiento | tipo libre + 1 % |

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

```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)
```
```output
                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.* Tres curvas mensuales en escala logarítmica. La ingenua termina en 5,44 veces el capital; la realista bruta, en 3,09; la neta de comisión, horquilla, impacto, préstamo y rebate (destacada), en 2,24. La distancia entre la ingenua y la realista se abre en 2007-2009; la del bruto al neto crece de forma continua.

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.

```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 $Y$ 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.**

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

2. Repite el backtest realista con 200 millones de USD de capital. ¿Qué costes cambian, cuánto y por qué?

**Solución.**

```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)
```
```output
               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 $Y$ el número sería otro, y por eso hay que variarlo.

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

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

   - A. 1 punto básico
   - B. 10 puntos básicos, porque manda el mínimo
   - C. 0,5 puntos básicos
   - D. 50 puntos básicos

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

   - A. Se multiplica por 4
   - B. Se multiplica por 2
   - C. No cambia
   - D. Se multiplica por 16

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

   - A. Porque se pierden posiciones largas
   - 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
   - C. Porque cambia el coste de la horquilla
   - D. Por un error de redondeo

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

   - A. La comisión
   - B. El impacto de mercado
   - C. La financiación
   - D. La horquilla

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

   - A. Ninguno, si el volumen total del año llega a ese tramo
   - 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
   - C. Sobrevalora el coste
   - D. Cambia la horquilla

   *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

### Research

- [Modelos Cuantitativos y Ejecución Algorítmica](https://pythonparatrading.com/modelos-cuantitativos-ejecucion-algoritmica/) · Research-0009 · 11 min
- [Introducción a la Volatilidad - Liquidez](https://pythonparatrading.com/introduccion-a-la-volatilidad-liquidez/) · Research-0001 · 6 min

### Documentación

- [Tóth et al. (2011): Anomalous price impact and the critical nature of liquidity in financial markets (Physical Review X)](https://doi.org/10.1103/PhysRevX.1.021006) · doi.org

### Libros

- [Bouchaud, Bonart, Donier y Gould (2018): Trades, Quotes and Prices. Cambridge University Press](https://doi.org/10.1017/9781316659335) · doi.org

### Datos

- [Datos del nivel 2: tarifas de tres brokers ficticios (CSV)](https://pythonparatrading.com/curso/datos/nivel-2/costes2_brokers.csv) · Descarga · costes2_brokers.csv
- [Datos del nivel 2: coste y disponibilidad del préstamo de títulos (CSV)](https://pythonparatrading.com/curso/datos/nivel-2/costes2_prestamo.csv) · Descarga · costes2_prestamo.csv

Lección anterior: [2.4.6 · LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos](https://pythonparatrading.com/curso-python-trading/nivel-2/lean-zipline-reloaded-y-bt/)

Lección siguiente: [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/)
