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.
- Módulo
- 2.4 · Backtesting: motores y paradigmas
- Duración
- 70 min
Lecciones del nivel 2
2.1 · Estadística y econometría para trading
- 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
- 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
- 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
- 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
- 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
- 2.1.6 Filtro de Kalman para hedge ratios dinámicos
- 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
- 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
2.2 · Anatomía de un sistema de trading
- 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
- 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
- 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
- 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
2.3 · Familias de estrategias
- 2.3.1 Seguimiento de tendencia y momentum
- 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
- 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
- 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
- 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
- 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
- 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
2.4 · Backtesting: motores y paradigmas
- 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
- 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
- 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
- 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
- 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
- 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
- 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
- 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
2.5 · Sesgos y validación
- 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
- 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
- 2.5.3 Validación cruzada combinatoria purgada para estrategias
- 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
- 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
- 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
- 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
2.6 · Métricas de rendimiento
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.
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]
| 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
import numpy as np
import pandas as pd
tarifas = pd.read_csv("costes2_brokers.csv")
tarifas.drop(columns=["tipo", "unidad"]) 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 NaNCuatro esquemas que se encuentran en brokers reales:
- Por acción con mínimo y máximo (
Alcor, planpor_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, planescalonado): 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):
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)['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.
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")7 tests correctosEn 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:
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) 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.0La 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
Alcorcuesta 20 pb y el fijo deBrio, 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.
Briocobra 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.
Cenitcuesta 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:
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) 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.48Una 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:
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) junio 2007 octubre 2008
tamano
pequeña 5.1 12.8
mediana 3.5 8.0
grande 2.4 5.0La 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.:
con las acciones de la orden, el volumen diario medio y la volatilidad diaria. El coeficiente 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 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:
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) 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.83La 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:
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"] 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 nulaLa 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:
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()){'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 | , 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 % |
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())227 meses: 2007-02-28 a 2025-12-31El 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:
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) 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.4159Leí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.
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 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
- Encuentra el importe a partir del cual
Brio(fijo por orden) es más barato que el plan por acción deAlcorpara una acción de 20 USD y para una de 100 USD. Explica por qué cambia.
Solución
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)")
breakprecio 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.
- Repite el backtest realista con 200 millones de USD de capital. ¿Qué costes cambian, cuánto y por qué?
Solución
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) 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.0533Solo 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 el número sería otro, y por eso hay que variarlo.
- 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
-
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
El plan por acción de
Alcorcobra 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.
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.
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.
¿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.
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.