Operar no es gratis
Un backtest que opera gratis sobrestima toda estrategia, y cuanto más opera, más miente. Los costes reales son la comisión del bróker, tasas de bolsa y regulatorias y, sobre todo, el diferencial compra-venta que se cruza cada vez. Suelen cotizarse en puntos básicos del valor operado: 1 pb es 0,01 %. Un viaje de ida y vuelta a 5 pb por lado cuesta 10 pb, algo que una estrategia que gana 3 pb al día no sobrevive si cambia de lado a diario.
def trade_cost(notional, bps):
return abs(notional) * bps / 10000
print(trade_cost(25000, 5)) # 12.5 buying 25,000 at 5 bps
print(trade_cost(-25000, 5)) # 12.5 selling costs the same
print(trade_cost(1_000_000, 1)) # 100.0 one basis point on a millionRotación: cuánto operas
El coste depende de cuánto cambia la posición, no de su tamaño. Con la posición como fracción de la cuenta, la rotación de un día es el cambio absoluto respecto a la posición anterior: abs(position[t] - position[t - 1]), partiendo de cero el día 0. Seguir largo no cuesta nada tras la primera compra; pasar de largo a corto opera dos veces la cuenta. Sumada en un año, la rotación es la primera cifra que mirar en cualquier estrategia.
import numpy as np
positions = np.array([1.0, 1.0, -1.0, -1.0, 0.0]) # long, long, flip to short, short, flat
turnover = np.abs(np.diff(positions, prepend=0.0)) # day 0 starts from a flat position
print(turnover) # [1. 0. 2. 0. 1.]
print(turnover.sum()) # 4.0 accounts traded over the five daysRendimientos netos y coste de equilibrio
El coste del día, como fracción de la cuenta, es turnover * bps / 10000, y el rendimiento neto es el bruto menos eso. Un número útil es el coste de equilibrio: el coste por unidad operada con el que el beneficio total es exactamente cero, sum(gross) / sum(turnover) * 10000. Compáralo con lo que pagas de verdad. Una estrategia con equilibrio de 2 pb está muerta en la práctica; una de 40 pb tiene margen.
# Gross and net returns of a rule that flips often, at 5 bps per unit traded
import numpy as np
rng = np.random.default_rng(11)
positions = np.where(rng.random(250) > 0.5, 1.0, -1.0) # a coin-flip position, flipping often
gross = rng.normal(0.0015, 0.005, size=250) # a strategy with a real daily edge before costs
turnover = np.abs(np.diff(positions, prepend=0.0))
net = gross - turnover * 5 / 10000
print(round(float(gross.sum()), 4), round(float(net.sum()), 4))
print(round(float(turnover.sum()), 1), "accounts traded;", round(float(gross.sum() / turnover.sum() * 10000), 2), "bps break-even")# The break-even cost of a small example
import numpy as np
gross = np.array([0.004, 0.002, 0.003, 0.001, 0.002])
positions = np.array([1.0, 1.0, -1.0, -1.0, 0.0])
turnover = np.abs(np.diff(positions, prepend=0.0))
breakeven = gross.sum() / turnover.sum() * 10000
print(round(float(breakeven), 2)) # 30.0 bps per unit traded
print(abs(round(float((gross - turnover * breakeven / 10000).sum()), 10))) # 0.0: the whole profit is gone- Los costes se cotizan en puntos básicos del valor operado:
abs(notional) * bps / 10000, igual en compra y venta. - La rotación es
abs(cambio de posición)(un giro opera 2); rendimiento neto = bruto - rotación * pb / 10000. - El coste de equilibrio
sum(gross) / sum(turnover) * 10000dice cuánto coste soporta una estrategia; compáralo con el real.
A continuación: el deslizamiento, la diferencia entre el precio esperado y el obtenido.
Lab: qué se califica
- Poner precio a una operación con
trade_cost25 pts - Contar lo operado con
turnover25 pts - Restar la factura con
net_returns25 pts - Hallar el coste que la anula con
breakeven_bps25 pts