Rejouer l'histoire
Un backtest demande ce qu'une règle de trading aurait rapporté sur des données passées. La version vectorisée travaille sur des tableaux entiers : pas de boucle sur les jours, juste un signal par jour, une position par jour et une multiplication avec les rendements. Elle est courte, rapide et facile à vérifier, ce qui en fait le premier outil pour trier des idées. Ses limites (ni ordres, ni liquidités, ni exécutions partielles) sont exactement ce qu'ajoute le sujet suivant.
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03]) # the asset's daily returns
signal = np.array([2.5, 0.4, -1.0, 0.0]) # any score: positive = buy, negative = sell
position = np.sign(signal) # +1, +1, -1, 0
print(position)Le décalage d'un jour
Un signal calculé avec la clôture d'aujourd'hui ne peut être négocié qu'après cette clôture : il gagne le rendement de demain, pas celui d'aujourd'hui. Dans les tableaux, c'est un décalage : strategy[t] = position[t - 1] * returns[t], sans position avant le premier jour. L'oublier permet à la stratégie de négocier sur un rendement qu'elle connaît déjà, et le backtest paraît magnifique. Le décalage tient en une ligne, la plus importante de toute la fonction.
# Yesterday's position earns today's return
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03])
position = np.array([1.0, 1.0, -1.0, 0.0])
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
print(strategy) # [ 0. 0.02 -0.01 -0.03]
print(position * returns) # the same-day version: [ 0.01 0.02 0.01 0. ] looks better, and is cheatingCapital et statistiques
Les rendements quotidiens se composent : la valeur après chaque jour est le capital initial multiplié par le produit cumulé des 1 + rendement. De cette courbe de capital, trois nombres résument un essai. Le rendement total est la valeur finale moins 1. Le ratio de Sharpe est le rendement quotidien moyen divisé par son écart-type, multiplié par sqrt(periods) pour l'annualiser. Le drawdown maximal est la pire chute depuis un sommet précédent, equity / running max - 1, c'est-à-dire la perte qu'un investisseur aurait vécue.
# Equity curve and the three statistics for a short run
import numpy as np
strategy = np.array([0.0, 0.02, -0.01, -0.03, 0.04])
equity = np.cumprod(1 + strategy)
print(np.round(equity, 5))
print(round(float(equity[-1] - 1), 5)) # total return
print(round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 3)) # annualised Sharpe
print(round(float((equity / np.maximum.accumulate(equity) - 1).min()), 5)) # maximum drawdown# A moving-average rule on a simulated price path, with its statistics
import numpy as np
rng = np.random.default_rng(7)
returns = rng.normal(0.0004, 0.01, size=500)
prices = 100 * np.cumprod(1 + returns)
fast = np.convolve(prices, np.ones(10) / 10, mode="valid")[-(len(prices) - 49):] # 10-day mean, aligned
slow = np.convolve(prices, np.ones(50) / 50, mode="valid") # 50-day mean
position = np.zeros(len(prices))
position[49:] = np.sign(fast - slow)
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
equity = np.cumprod(1 + strategy)
drawdown = equity / np.maximum.accumulate(equity) - 1
print(round(float(equity[-1] - 1), 4), round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 4), round(float(drawdown.min()), 4))- Position = signe du signal ; le rendement de la stratégie est
position[t - 1] * returns[t]: le rendement d'aujourd'hui revient à la décision d'hier. - Le capital est
start * cumprod(1 + r); rendement total, Sharpe annualisé et drawdown maximal le résument. - Les backtests vectorisés sont des filtres rapides : ils ignorent coûts et exécutions, et de nombreux essais sur un même historique produisent de la chance.
À suivre : un backtester événementiel, rejouer l'histoire barre par barre avec liquidités, ordres et exécutions.
Lab : ce qui est noté
- Transformer un signal en position avec
signal_to_position25 pts - Gagner la position d'hier avec
strategy_returns25 pts - Composer les jours avec
equity_curve25 pts - Résumer l'essai avec
backtest_stats25 pts