Négocier n'est pas gratuit
Un backtest qui négocie gratuitement surestime toute stratégie, et plus elle négocie, plus il ment. Les coûts réels sont la commission du courtier, les frais de bourse et de régulation, et surtout le écart acheteur-vendeur franchi à chaque fois. On les cote généralement en points de base de la valeur négociée : 1 pb vaut 0,01 %. Un aller-retour à 5 pb dans chaque sens coûte 10 pb, qu'une stratégie gagnant 3 pb par jour ne survit pas si elle bascule chaque jour.
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 millionRotation : combien vous négociez
Le coût dépend de la variation de la position, pas de sa taille. Avec la position en fraction du compte, la rotation d'un jour est la variation absolue par rapport à la position précédente : abs(position[t] - position[t - 1]), à partir d'une position nulle au jour 0. Rester acheteur ne coûte rien après le premier achat ; passer d'acheteur à vendeur négocie deux fois le compte. Additionnée sur une année, la rotation est le premier chiffre à regarder dans toute stratégie.
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 daysRendements nets et coût d'équilibre
Le coût du jour, en fraction du compte, vaut turnover * bps / 10000, et le rendement net est le rendement brut moins ce coût. Un nombre utile est le coût d'équilibre : le coût par unité négociée pour lequel le profit total est exactement nul, sum(gross) / sum(turnover) * 10000. Comparez-le à ce que vous payez vraiment. Une stratégie à 2 pb d'équilibre est morte en pratique, une à 40 pb a de la marge.
# 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- Les coûts se cotent en points de base de la valeur négociée :
abs(notional) * bps / 10000, identique à l'achat et à la vente. - La rotation est
abs(variation de position)(un retournement vaut 2) ; rendement net = brut - rotation * pb / 10000. - Le coût d'équilibre
sum(gross) / sum(turnover) * 10000dit combien de coût une stratégie supporte ; comparez-le au coût réel.
À suivre : le glissement, l'écart entre le prix attendu et le prix obtenu.
Lab : ce qui est noté
- Chiffrer une transaction avec
trade_cost25 pts - Compter ce qui est négocié avec
turnover25 pts - Déduire la facture avec
net_returns25 pts - Trouver le coût qui l'annule avec
breakeven_bps25 pts