Une barre à la fois
Un backtester événementiel se comporte comme un système réel : le temps avance barre par barre, et à chaque barre le programme ne voit que le passé, décide, envoie un ordre et met à jour son compte. Il est plus lent qu'un backtest vectorisé, mais il garde un état (liquidités, titres), peut modéliser ordres et exécutions, et le même code de stratégie pourra ensuite tourner sur un flux en direct. La boucle est l'essentiel : un for sur les barres, le compte passant d'une itération à la suivante.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0 # not enough history yet
fast_mean = sum(history[-fast:]) / fast
slow_mean = sum(history[-slow:]) / slow
return 1 if fast_mean > slow_mean else 0
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
print(crossover_signal(prices[:3], 2, 4)) # 0: only 3 prices
print(crossover_signal(prices[:6], 2, 4)) # 1: the 2-bar mean is above the 4-bar mean
print(crossover_signal(prices[:9], 2, 4)) # 0: the trend has turnedLes ordres changent le compte
Un ordre transforme des liquidités en titres ou l'inverse. Avec des titres entiers, acheter int(cash // price) laisse un peu de liquidités, et la valeur du compte est toujours cash + shares * price. Garder la logique d'ordre dans une petite fonction, execute_order, raccourcit la boucle et permet d'ajouter frais et glissement plus tard à un seul endroit. Tout vendre, c'est cash + shares * price et zéro titre.
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price) # whole shares only
return cash - bought * price, shares + bought
return cash + shares * price, 0
cash, shares = execute_order(1000.0, 0, 103.0, 1)
print(cash, shares, cash + shares * 103.0) # 73.0 9 1000.0
cash, shares = execute_order(cash, shares, 99.0, 0)
print(cash, shares) # 964.0 0La boucle et ce qu'elle peut savoir
À la barre t, la stratégie peut utiliser prices[:t], qui s'arrête à la clôture précédente, et négocie à prices[t]. Découper prices[:t] garde le futur dehors : le prix suivant n'est tout simplement pas dans la liste. Un bon test d'un moteur événementiel en découle : l'exécuter sur les k premières barres seulement doit donner les k premiers résultats de l'exécution complète. Sinon, la stratégie a triché.
def crossover_signal(history, fast, slow):
if len(history) < slow:
return 0
return 1 if sum(history[-fast:]) / fast > sum(history[-slow:]) / slow else 0
def execute_order(cash, shares, price, target):
if target == 1:
bought = int(cash // price)
return cash - bought * price, shares + bought
return cash + shares * price, 0
def run_backtest(prices, fast, slow, cash):
shares = 0
values = []
for t, price in enumerate(prices):
if t > 0:
target = crossover_signal(prices[:t], fast, slow) # only what was known at the last close
cash, shares = execute_order(cash, shares, price, target)
values.append(cash + shares * price)
return values
prices = [100, 101, 103, 102, 105, 107, 104, 103, 99, 98]
full = run_backtest(prices, 2, 4, 1000.0)
print([round(v, 2) for v in full])
print(run_backtest(prices[:6], 2, 4, 1000.0) == full[:6]) # True: the future never leaks# The benchmark: buy once at the start and hold
import random
def buy_and_hold(prices, cash):
shares = int(cash // prices[0])
left = cash - shares * prices[0]
return [left + shares * price for price in prices]
random.seed(3)
prices = [100.0]
for _ in range(99):
prices.append(round(prices[-1] * (1 + random.gauss(0.0004, 0.012)), 2))
held = buy_and_hold(prices, 10000.0)
print(round(held[0], 2), round(held[-1], 2))- Un backtester événementiel boucle sur les barres avec un état (liquidités, titres) ; à la barre
til ne peut utiliser queprices[:t]. - Les ordres font passer entre liquidités et titres entiers : la valeur du compte est
cash + shares * price; gardez la logique d'ordre dans une fonction. - Testez le look-ahead : une exécution sur les
kpremières barres doit égaler leskpremiers résultats de l'exécution complète ; comparez à l'achat-conservation.
À suivre : le biais de anticipation, l'erreur silencieuse qui rend un backtest trop beau pour être vrai.
Lab : ce qui est noté
- Comparer deux moyennes avec
crossover_signal25 pts - Exécuter l'ordre avec
execute_order25 pts - Parcourir les barres avec
run_backtest25 pts - Fixer la référence avec
buy_and_hold25 pts