σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Parcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Python›Module 6 · Backtesting et coûts de trading›Sujet 6.2·~1 h 15 minAvancé

Un backtester événementiel

Position dans le parcoursModule 6 sur 11 · Sujet 2 sur 6
6.2 Un backtester événementiel
—Score du lab— / 100XP total—Série—

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 turned

Les 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 0

La 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))
Le même code peut passer en réelLa boucle voit une barre et agit : c'est exactement la forme d'un programme de trading en direct, où la barre suivante arrive d'un flux de données plutôt que d'une liste. Séparer signal, ordre et compte dans des fonctions distinctes permet de remplacer le flux historique par un flux réel sans réécrire la stratégie.
  • Un backtester événementiel boucle sur les barres avec un état (liquidités, titres) ; à la barre t il ne peut utiliser que prices[: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 k premières barres doit égaler les k premiers 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
Explorateur · lab-88-1
▾ lab-88-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-88-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-88-1 revient aux fichiers de départ et le terminal est vidé. Votre solution actuelle sera perdue : cette action est irréversible.

Sera réinitialisé
  • main.py → code de départ
  • Historique du terminal et sortie des tests
  • Modifications non soumises
Sera conservé
  • Meilleur score du lab
  • Votre XP et votre série
  • Progression du cours et notes

Connectez-vous pour exécuter votre code.

Tous les cours sont gratuits à lire. Créez un compte ou connectez-vous pour exécuter du code, lancer les tests et soumettre ce lab.

  • IDE dans le navigateur avec terminal bash — rien à installer
  • Correction instantanée, XP et votre place dans le parcours
Créer un compteSe connecter
OU
Continuer avec GitHub