<< Sommaire QC | Précédent : QC-Py-08-Multi-Asset-Strategies << | Suivant : QC-Py-10-Risk-Portfolio-Management >>

QC-Py-09 : Types d’Ordres et Order Management dans QuantConnect

Duree estimee : 75 minutes

Objectifs d’apprentissage : 1. Maitriser les ordres de base (Market, Limit) et leur utilisation 2. Comprendre les ordres stop (Stop Market, Stop Limit) pour la gestion du risque 3. Utiliser les ordres speciaux (MOO, MOC, LIT) pour des exécutions precises 4. Gerer les ordres avec les Order Tickets (modification, annulation, statuts) 5. Implementer des stratégies avec bracket orders et trailing stops

Prerequis : - Notebooks QC-Py-01 a QC-Py-08 completes - Comprehension de base des marches financiers - Notions de Python intermediaire

Note de conception : Ce notebook contient du code de reference a copier dans QuantConnect Lab (main.py). Les cellules ne sont pas prevues pour etre executees en tant que notebook Jupyter. L’absence d’outputs (execution_count: null) est intentionnelle.

Pré-requis matériel : ce notebook est un catalogue d’ordres QuantConnect Python. Il est conçu pour être lu linéairement (du Market Order au Bracket) OU parcours thématiques : - Risk management : Parties 2 (Stop) + 5 (Bracket). - Stratégies execution-aware : Parties 4 (Update/Cancel) + 6 (Combo). - Protocole complet : Parties 1 → 6.

Série QC-Python (parcours pédagogique) : 1. QC-Py-01 Setup : environnement local (kernels, .NET Interactive). 2. QC-Py-02 Platform Fundamentals : Initialize, OnData, History. 3. QC-Py-03 Data Management : self.AddEquity, universes. 4. QC-Py-04 Research Workflow : notebooks Jupyter vs main.py. 5. QC-Py-05 Universe Selection : coarse/fine universes. 6. QC-Py-06 Options Trading : options Greeks, chains. 7. QC-Py-07 Futures/Forex : contrats, rollover, leverage. 8. QC-Py-08 Multi-Asset Strategies : cross-asset correlation. 9. QC-Py-09 (VOUS ÊTES ICI) : Order Types. 10. QC-Py-10 Risk Portfolio Management : position sizing, stops. 11. QC-Py-11 Technical Indicators : SMA, EMA, ATR. 12. QC-Py-12 Backtesting Analysis : metrics, attributions.

[REFERENCE QC Cloud] Ce notebook illustre du code QuantConnect a executer dans l’IDE Cloud (https://www.quantconnect.com/research). L’environnement local ne dispose pas de QuantBook ni de l’historical data feed. Pour executer : cloner le projet QC associe, ouvrir research.ipynb, executer cellule par cellule.

Pourquoi ce marqueur [REFERENCE QC Cloud] : le code de QuantConnect ne s’exécute pas dans un kernel Jupyter standard — il est déployé sur QC Cloud via lean cloud push puis backtesté. Les cellules [REFERENCE QC] sont des gabarits à copier dans main.py d’un projet QC Lab. Les démos Python locales (cellules sans le marqueur) s’exécutent dans le kernel et illustrent le comportement algorithmique hors QC.

Trois familles de cellules dans ce notebook : 1. Gabarits QC (marquées [REFERENCE QC]) — à déployer tels quels. 2. Démos Python locales — exécutables ici, conceptualisent l’API. 3. Cellules d’exercice — stubs pass à compléter par l’étudiant.

Coût-bénéfice : un backtest QC Cloud consomme ~5-15 minutes pour une période 10 ans. Les démos Python locales sont instantanées et permettent d’itérer sur la logique avant déploiement.


Mode d’emploi : Ce notebook est un document de reference (non executable). Le code QCAlgorithm presente ici est a copier dans main.py de votre projet QuantConnect Lab. Ne tentez pas de faire “Run All” – les imports AlgorithmImports n’existent pas en Jupyter local.

Pour demarrer un projet QC : copiez un main.py depuis projects/ ou partner-course-quant-trading/templates/, puis adaptez-le.


Introduction

Ce notebook couvre les sept types d’ordres principaux sur QuantConnect et leur gestion du cycle de vie. C’est la deuxième brique du pipeline QC Python (après QC-Py-02 Platform Fundamentals).

Pré-requis : - QC-Py-01 Setup (environnement local) - QC-Py-02 Platform Fundamentals (Initialize, OnData, History) - Python intermédiaire (classes, décorateurs)

Objectifs pédagogiques : 1. Choisir le bon type d’ordre selon la stratégie (exécution immédiate vs attente vs stop suiveur). 2. Comprendre le cycle de vie d’un ordre (Submitted → Filled/Canceled). 3. Maîtriser les modifications (UpdateFields) et annulations. 4. Combiner les ordres en bracket/combo pour le risk management. 5. Éviter les race conditions (UpdateQuantity après Fill partiel).

Public cible : ingénieurs ML/quant junior qui prototypent une première stratégie execution-aware.


Plan du notebook

  1. Ordres de Base (25 min) - Market Orders, Limit Orders
  2. Ordres Stop (20 min) - Stop Market, Stop Limit
  3. Ordres Speciaux (20 min) - MOO, MOC, LIT
  4. Order Management (25 min) - Tickets, Statuts, Events
  5. Combo Orders (15 min) - Bracket Orders, Best Practices
  6. Stratégie Complete (20 min) - Breakout Strategy

6 parties progressives, durée totale ~2h30 :

Partie Type d’ordre Durée Compétence visée
1 Market + Limit + Statut cycle 25 min Exécution de base
2 Stop + Stop-Limit + Trailing 20 min Protection position
3 StopMarket + LIT + exercices 20 min Ordres spéciaux
4 Update/Cancel + OnOrderEvent 25 min Gestion dynamique
5 Bracket + OCO + bonnes pratiques 15 min Combinaisons
6 Stratégie complète + exercices 20 min Assemblage

Conseil de lecture : si vous connaissez déjà MarketOrder et LimitOrder (parties 1), sautez directement à la partie 4 (gestion du cycle de vie) puis revenez aux exercices 1-3 pour valider.


Vue d’ensemble des Types d’Ordres

Type d’Ordre Méthode Exécution Usage Principal
Market MarketOrder() Immediate Entrees/sorties urgentes
Limit LimitOrder() Si prix atteint Acheter moins cher
Stop Market StopMarketOrder() Market si stop touche Stop-loss
Stop Limit StopLimitOrder() Limit si stop touche Stop avec contrôle
MOO/MOC MarketOnOpen/CloseOrder() Open/Close Timing spécifique
LIT LimitIfTouchedOrder() Limit si trigger Pullback entry

Les 7 types d’ordres en un coup d’œil

Type Usage principal Garantie d’exécution Contrôle du prix
Market Entrée immédiate, day-trading Oui (slippage) Non
Limit Entrée/reprise à prix cible Non (non-fill possible) Oui
Stop Market Stop-loss agressif Oui (slippage) Non
Stop Limit Stop-loss avec contrôle prix Non (gap risk) Oui (partiel)
Trailing Stop Capture tendance, stop suiveur Oui (au déclenchement) Partiel
LIT Entrée breakout/pullback Non Oui (post-trigger)
MarketOnOpen/Close Swing, ouverture/clôture Oui (slippage) Non

Couverture des cas : - 80% des stratégies utilisent uniquement Market + Limit. - 15% ajoutent Stop Market/Limit pour le risk management. - 5% utilisent Trailing Stop, LIT, ou des combos avancés.

À retenir : un type d’ordre n’est pas “meilleur” qu’un autre — il est adapté à un contexte. Market pour la vitesse, Limit pour le prix, Stop pour la protection, Trailing pour la tendance.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Reference rapide: Tous les types d'ordres
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class OrderTypesQuickReference(QCAlgorithm):
    """
    Reference rapide de tous les types d'ordres QuantConnect.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
    
    def ShowAllOrderTypes(self, price):
        """Demonstration de tous les types d'ordres."""
        
        # 1. MARKET ORDER - Execution immediate
        self.MarketOrder(self.spy, 100)  # Acheter
        self.MarketOrder(self.spy, -50)  # Vendre
        
        # 2. LIMIT ORDER - Prix specifique
        self.LimitOrder(self.spy, 100, price * 0.98)   # Acheter si prix <= 98%
        self.LimitOrder(self.spy, -100, price * 1.02)  # Vendre si prix >= 102%
        
        # 3. STOP MARKET ORDER - Declenche market order
        self.StopMarketOrder(self.spy, -100, price * 0.95)  # Stop-loss
        self.StopMarketOrder(self.spy, 100, price * 1.05)   # Breakout entry
        
        # 4. STOP LIMIT ORDER - Declenche limit order
        self.StopLimitOrder(self.spy, -100, price * 0.95, price * 0.94)
        
        # 5. MARKET ON OPEN/CLOSE
        self.MarketOnOpenOrder(self.spy, 100)   # A l'ouverture
        self.MarketOnCloseOrder(self.spy, -100) # A la cloture
        
        # 6. LIMIT IF TOUCHED
        self.LimitIfTouchedOrder(self.spy, 100, price * 0.98, price * 0.99)
        
        # 7. SET HOLDINGS (allocation par %)
        self.SetHoldings(self.spy, 0.5)   # 50% du portfolio
        self.SetHoldings(self.spy, -0.3)  # Short 30%

Résultats du backtest QC Cloud (OrderTypesQuickReference)

Metrique Valeur
Dates negociables 2516
Ordres executes 0
Sharpe Ratio 0
CAGR 0%
Max Drawdown 0%
Net Profit 0%
Equity finale $100,000
PSR 0%

Quick reference of all order types (market, limit, stop, MOO/MOC, LIT, SetHoldings), SPY. Classe de reference (sans OnData / sans ordre emis) : aucun trade execute. Backtest QC Cloud 2015-2024 (projet 33181748, bt 4c4734dc00a7693da39c73d1072af0b6).

Pourquoi le choix du type d’ordre est critique

Le type d’ordre détermine : - Le prix d’exécution (fixé à l’avance vs marché). - Le timing (immédiat vs conditionnel). - Le contrôle du slippage (garanti vs worst-case). - Le coût (market = taker fees, limit = maker fees).

Trade-off fondamental : - Market : exécution garantie, prix inconnu → sur-coût slippage. - Limit : prix garanti, exécution incertaine → risque non-fill. - Stop : déclenchement conditionnel → protection stop-loss.

Règle d’or : 80% des stratégies retail utilisent uniquement Market et Limit. Les ordres Stop/Limit/Trailing sont les garde-fous de la gestion du risque (cf. QC-Py-10 Risk Management).


PARTIE 1 : Ordres de Base (25 min)

Les deux types d’ordres fondamentaux en trading algorithmique :

  1. Market Order — exécution immédiate au meilleur prix disponible.
  2. Limit Order — exécution à un prix seuil ou mieux.

Ces deux types couvrent ~80% des cas d’usage. Les parties suivantes ajoutent des ordres conditionnels (Stop, Trailing) et des combinaisons (Bracket, OCO).

Pré-requis pour cette partie : aucun au-delà de QC-Py-02.

Outputs attendus : - 1 backtest MarketOrderExample (référence QC). - 1 backtest LimitOrderExample (référence QC, voir cellule #15). - 1 exercice pratique : stratégie d’entrée avec limit orders conditionnels (cellule #19).

1.1 Market Orders

Un Market Order s’execute immediatement au meilleur prix disponible.

Avantage Inconvenient
Exécution garantie Pas de contrôle sur le prix
Rapidite Slippage possible
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Exemple de Market Orders
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MarketOrderExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.example_executed = False
    
    def OnData(self, data):
        if self.example_executed or not data.ContainsKey(self.spy):
            return
        
        price = data[self.spy].Close
        
        # MarketOrder: Acheter exactement 50 actions
        ticket = self.MarketOrder(self.spy, 50)
        self.Log(f"MarketOrder: Buy 50 SPY @ ~${price:.2f}")
        self.Log(f"Order ID: {ticket.OrderId}")
        
        self.example_executed = True
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            self.Log(f"[FILLED] {orderEvent.FillQuantity} @ ${orderEvent.FillPrice:.2f}")

Résultats du backtest QC Cloud (MarketOrderExample)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio -0.427
CAGR 1.871%
Max Drawdown 4.900%
Net Profit 20.4%
Equity finale $120,367
PSR 8.0%

Market order example (buy 50 SPY), daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt 43580311a0c1c0f9ef659f47f79fe5b7). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

Deux API pour ouvrir une position

QuantConnect expose deux API principales pour prendre une position :

  1. self.MarketOrder(symbol, qty) — envoie un ordre market sur le symbol avec une quantité fixe. API bas-niveau, contrôle total.
  2. self.SetHoldings(symbol, pct) — ajuste la position pour atteindre un pourcentage cible du portefeuille. API haut-niveau, le framework calcule la quantité.

Quand utiliser l’un ou l’autre :

Cas d’usage API recommandée
Scalping, HFT, taille fixe MarketOrder(qty)
Rebalance portfolio, sizing % SetHoldings(pct)
Risk parity, vol targeting SetHoldings(pct)
Stratégie momentum (entry/exit discrets) MarketOrder(qty)

Piége classique : SetHoldings(0.5) puis SetHoldings(0.5) 1 minute plus tard n’achète pas deux fois — le framework calcule le delta pour maintenir 50%, donc rien ne se passe si la position est déjà à 50%.

MarketOrder vs SetHoldings - Deux Approches pour l’Exécution

MarketOrder : Contrôle direct de la quantité

self.MarketOrder("SPY", 50)  # Acheter exactement 50 actions
  • Avantage : Précision absolue du nombre d’actions
  • Usage : Stratégies avec quantités fixes, pyramiding

SetHoldings : Allocation par pourcentage du portefeuille

self.SetHoldings("SPY", 0.5)  # Allouer 50% du portfolio
  • Avantage : Ajustement automatique selon la valeur du portefeuille
  • Usage : Allocation stratégique, rebalancement

Calcul interne de SetHoldings :

target_value = portfolio_value × target_percentage
current_holdings = quantity × current_price
delta = target_value - current_holdings
quantity_to_trade = delta / current_price

Quand utiliser quoi ? - SetHoldings : “Je veux 50% en SPY” - MarketOrder : “Je veux acheter exactement 100 actions”

Trade-off : contrôle brut vs cohérence portefeuille

MarketOrder(qty) contrôle la quantité exacte mais ignore le contexte portefeuille. SetHoldings(pct) atteint un % cible mais le framework calcule le delta nécessaire.

Exemple chiffré : - Portefeuille $100k, 60% actions, 40% bonds. - Signal “réduire AAPL de 5% à 3%” du portefeuille.

Avec MarketOrder : 1. Calcul manuel : AAPL à 200$, holdings actuel 5000$, cible 3000$. 2. self.MarketOrder(self.aapl, -10) (vendre 10 actions = 2000$). 3. Risque : si le prix bouge entre la décision et l’exécution, le delta est faux → sur/sous-exécution.

Avec SetHoldings : 1. self.SetHoldings(self.aapl, 0.03) (3% du portefeuille). 2. Le framework calcule la quantité nécessaire : delta = target - current. 3. Avantage : auto-ajusté au prix courant, au portefeuille courant.

Recommandation : - Stratégies de portefeuille (rebalance quotidien) : SetHoldings. - Stratégies tactiques (entry/exit discrets) : MarketOrder/LimitOrder.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# SetHoldings: Allocation par pourcentage
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class SetHoldingsExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
    
    def OnData(self, data):
        if self.Portfolio.Invested:
            return
        
        # Allouer 50% a SPY et 30% a QQQ
        self.SetHoldings(self.spy, 0.5)  # 50%
        self.SetHoldings(self.qqq, 0.3)  # 30%
        # 20% reste en cash
        
        self.Log(f"Portfolio allocated: SPY 50%, QQQ 30%, Cash 20%")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            symbol = orderEvent.Symbol.Value
            self.Log(f"[{symbol}] Filled: {orderEvent.FillQuantity} @ ${orderEvent.FillPrice:.2f}")

Résultats du backtest QC Cloud (SetHoldingsExample)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.568
CAGR 13.356%
Max Drawdown 27.600%
Net Profit 250.3%
Equity finale $350,304
PSR 12.4%

SetHoldings allocation (SPY 50%, QQQ 30%, Cash 20%), daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt 240930f7da5c59fb93f22efb8afd4c0c). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

Pourquoi les limit orders sont essentiels

Trois raisons principales d’utiliser un Limit Order plutôt qu’un Market Order :

  1. Contrôle du prix — vous spécifiez le prix maximum/minimum acceptable. Pas de slippage négatif au-delà du seuil.
  2. Coût réduit — les LimitOrder paient les maker fees (souvent 0.1-0.3 bps chez les brokers ECN), contre taker fees plus élevés (1-3 bps) pour les Market Order.
  3. Discipline — un limit order refusé n’est pas exécuté. Vous n’accumulez pas de positions impulsives.

Inconvénient majeur : un limit order peut ne pas être fillé. Si le marché ne touche jamais votre prix, vous restez hors position. C’est le trade-off fundamental : exécution garantie (market) vs prix garanti (limit).

Stratégie typique : placer un limit order 0.1-0.5% sous le dernier prix pour les entrées long, avec une expiration de 1-4 heures.

1.2 Limit Orders

Un Limit Order ne s’execute que si le prix atteint le niveau specifie.

Direction Limit Price Exécution
Achat Prix maximum Si marche <= limit
Vente Prix minimum Si marche >= limit

Un Limit Order spécifie un prix maximum (achat) ou minimum (vente). L’ordre est exécuté uniquement si le marché atteint ce prix ou mieux.

Syntaxe QuantConnect :

self.LimitOrder(self.spy, 100, self.spy.Price * 0.99)  # limit buy -1%

Cycle de vie : 1. Submitted — l’ordre est envoyé au broker. 2. PartiallyFilled (optionnel) — fill partiel si la quantité est importante et la liquidité limitée. 3. Filled — totalement exécuté au prix limite ou mieux. 4. Canceled — annulé manuellement ou par expiration.

À noter : LimitOrder avec prix égal au dernier prix est équivalent à un Market Order agressif. Pour garantir l’exécution, utilisez LimitOrder à last_price + slippage_tolerance (achat).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Exemple de Limit Orders
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class LimitOrderExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.buy_ticket = None
        self.order_placed = False
    
    def OnData(self, data):
        if not data.ContainsKey(self.spy):
            return
        
        current_price = data[self.spy].Close
        
        # Placer un limit order une seule fois
        if not self.order_placed:
            limit_price = current_price * 0.99  # 1% sous le prix actuel
            
            self.buy_ticket = self.LimitOrder(self.spy, 100, limit_price)
            
            self.Log(f"Limit Order: Buy 100 @ ${limit_price:.2f}")
            self.Log(f"Current Price: ${current_price:.2f}")
            
            self.order_placed = True
        
        # Verifier le statut
        if self.buy_ticket:
            if self.buy_ticket.Status == OrderStatus.Filled:
                self.Log(f"Filled at ${self.buy_ticket.AverageFillPrice:.2f}")
    
    def OnOrderEvent(self, orderEvent):
        self.Log(f"Order {orderEvent.OrderId}: {orderEvent.Status}")

Résultats Backtest QC Cloud - LimitOrderExample (2015-2024, $100,000 initial)

Runtime Error (backtest QC Cloud, bt 94fdaf130de8f55846fcd1362d2beed3). La stratégie de reference place un limit order a -1% sous le prix, puis s’arrete, puis Runtime Error a 2015-03-20 : 'NoneType' object has no attribute 'Close' a data[self.spy].Close MALGRE le guard if not data.ContainsKey(self.spy): return – gotcha QC : ContainsKey renvoie True mais la valeur peut etre None sur certaines bars (barres auxiliaires split/dividende). Le backtest s’arrete prematurement ; les statistiques partielles sur la periode anterieure a l’erreur ne sont pas un résultat 10y significatif et ne sont pas commitees (G.2 : aucune metrique non-verifiable). La table fabriquee d’origine (“Q1 2023”, Sharpe 0, 0 trade) est supprimee.

Backtest QC Cloud 2015-2024 (projet 33177683, bt 94fdaf130de8f55846fcd1362d2beed3).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Statuts des ordres
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class OrderStatusExample(QCAlgorithm):
    """Demonstration des differents statuts d'ordre."""
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
    
    def OnOrderEvent(self, orderEvent):
        """Gerer tous les statuts possibles."""
        status = orderEvent.Status
        
        if status == OrderStatus.New:
            self.Log("Ordre cree")
        elif status == OrderStatus.Submitted:
            self.Log("Ordre soumis au marche")
        elif status == OrderStatus.PartiallyFilled:
            self.Log(f"Partiellement rempli: {orderEvent.FillQuantity}")
        elif status == OrderStatus.Filled:
            self.Log(f"Execute: {orderEvent.FillQuantity} @ ${orderEvent.FillPrice:.2f}")
        elif status == OrderStatus.Canceled:
            self.Log("Ordre annule")
        elif status == OrderStatus.Invalid:
            self.Log(f"Ordre invalide: {orderEvent.Message}")

Résultats du backtest QC Cloud (OrderStatusExample)

Metrique Valeur
Dates negociables 2516
Ordres executes 0
Sharpe Ratio 0
CAGR 0%
Max Drawdown 0%
Net Profit 0%
Equity finale $100,000
PSR 0%

Order status demo (buy 100 SPY market), daily resolution. Classe de reference (sans OnData / sans ordre emis) : aucun trade execute. Backtest QC Cloud 2015-2024 (projet 33181748, bt b4bf89c7aac2170fe045a814cbe981b0).

Le piège du statut “Submitted” prolongé

Un LimitOrder reste en statut Submitted tant qu’il n’est ni fillé ni annulé. Cela peut durer : - Quelques secondes (limit proche du marché). - Quelques heures (limit 5% sous le marché sur action volatile). - Plusieurs jours (limit sur option OTM, jamais fillé).

Trois implications pratiques :

  1. Comptage faux : self.Transactions.GetOpenOrders() renvoie tous les Submitted. Ne les comptez pas comme des positions ouvertes.
  2. Capital bloqué : un LimitOrder ne bloque pas le cash (achat non-fillé), mais bloque le pouvoir d’achat sur certains brokers (Pattern Day Trader rules, marge).
  3. Annulation requise : si vous changez d’avis, vous DEVEZ annuler explicitement (self.Transactions.CancelOrder(orderId)).

Bonne pratique : toujours associer un expiry à un limit order (SetHoldings ou TimeInForce.GoodTilDate) pour éviter les ordres zombies qui traînent 3 jours après votre décision.

Gestion des Statuts d’Ordres - Cycle de Vie Complet

Chaque ordre QuantConnect traverse une séquence d’états avant finalisation.

Séquence normale :

New → Submitted → PartiallyFilled* → Filled

Séquence avec annulation :

New → Submitted → Canceled

États et signification :

État Moment Action
New Création locale Aucune (automatique)
Submitted Envoyé au marché Attendre
PartiallyFilled Exécution partielle Peut continuer ou annuler
Filled Complété Mettre à jour positions
Canceled Annulé Capital libéré
Invalid Erreur Corriger et renvoyer

Pourquoi gérer PartiallyFilled ? Les gros ordres peuvent être fractionnés par le marché : - Ordre de 1000 actions de SPY - Exécution : 300 @ 150.00, puis 400 @ 150.01, puis 300 @ 150.02 - Important : 3 événements PartiallyFilled avant Filled

Bonnes pratiques OnOrderEvent : - Logger TOUJOURS le statut pour debugging - Gérer le cas où seulement une partie est fillée - Annuler les ordres restants en cas d’erreur

Exercice 1 : Stratégie d’Entree avec Limit Orders Conditionnels

Cet exercice combine les limit orders avec une logique conditionnelle simple.

Objectif : Implementer une fonction should_place_limit_order(current_price, sma_value, rsi_value) qui decide si un limit order d’achat doit etre place, et retourne le prix limite optimal.

Règles : - Si RSI < 35 (survente) et prix < SMA : acheter avec un limit a prix - 0.5% - Si RSI < 35 et prix >= SMA : acheter avec un limit a prix - 1% (plus agressif, car au-dessus de la moyenne) - Sinon : ne pas acheter (retourner None)

Indices : - La fonction retourne soit un float (le prix limite), soit None (pas d’ordre) - Verifiez que le prix limite est positif avant de le retourner

Variante (sans stub fourni) — la même logique en stratégie complète :

Objectif : coder une stratégie qui place un limit order d’entrée uniquement quand certaines conditions sont réunies.

Énoncé : - Symbol : SPY. - Timeframe : daily. - Condition d’entrée : close > SMA(20) (tendance haussière). - Limite : placer un limit buy à close - 1% (pullback). - Exit : si le limit n’est pas fillé en 5 jours, annuler.

Squelette :

class LimitConditionalStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2024, 1, 1)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.sma = self.SMA(self.spy, 20, Resolution.Daily)
        self.limit_ticket = None
        self.entry_day = None

    def OnData(self, data):
        if self.sma.IsReady and self.limit_ticket is None:
            price = self.Securities[self.spy].Price
            if price > self.sma.Current.Value:
                # Place limit buy at -1% below current
                limit_price = price * 0.99
                self.limit_ticket = self.LimitOrder(self.spy, 100, limit_price)
                self.entry_day = self.Time
        # Expire after 5 days
        if self.limit_ticket is not None and self.Time.day - self.entry_day.day > 5:
            self.Transactions.CancelOrder(self.limit_ticket.OrderId)
            self.limit_ticket = None

Hints : - self.Time.day est le jour du mois, pas un delta. Utilisez self.Time - self.entry_day pour un timedelta. - Pour comparer à SMA, self.sma.IsReady doit être True (20 barres). - self.limit_ticket.OrderId est nécessaire pour CancelOrder.

Critères de validation : - Pas d’erreur d’exécution. - Au moins 1 trade sur la période 2020-2024. - Le limit est annulé après 5 jours si non-fillé.

# Exercice 1 : Strategie d'Entree avec Limit Orders Conditionnels
# TODO etudiant : implementer should_place_limit_order

def should_place_limit_order(current_price, sma_value, rsi_value):
    """
    Decide si un limit order d'achat doit etre place et retourne le prix limite.

    Parameters:
    -----------
    current_price : float
    sma_value : float (Simple Moving Average)
    rsi_value : float (RSI indicator)

    Returns:
    --------
    float or None : prix limite si ordre a placer, None sinon
    """
    result = None  # TODO etudiant : implementer la logique conditionnelle
    return result

print("Exercice a completer")

PARTIE 2 : Ordres Stop (20 min)

2.1 Stop Market Orders

Un Stop Market Order devient un market order quand le stop price est atteint — c’est l’ordre « feu à volonté » : dès que le prix touche le seuil, un Market Order est émis, sans prix limite ni contrôle du slippage.

Direction Stop Price Declenchement
Vente (Stop-Loss) Sous le prix actuel Si prix <= stop
Achat (Breakout) Au-dessus du prix Si prix >= stop

Cas d’usage canonique : stop-loss d’urgence sur position overnight — si le marché gap -3%, la position est automatiquement coupée au marché.

Anti-pattern : ne JAMAIS utiliser un Stop Market sur une action illiquide — le slippage d’un gap overnight peut atteindre 10-20% sur small caps (l’alternative contrôlée est le Stop Limit, 2.2).

Note API : self.StopMarketOrder(symbol, qty, stop_price) — stop_price est le seuil de déclenchement, pas le prix d’exécution.

Avant les ordres Stop — ce que vous maîtrisez déjà

Vous maîtrisez maintenant : - MarketOrder : exécution immédiate, prix inconnu. - LimitOrder : prix garanti, exécution incertaine. - Statut cycle de vie : Submitted → PartiallyFilled → Filled/Canceled.

Question pédagogique : que faire quand vous êtes déjà en position et voulez limiter la perte sans fixer le prix exact ?

Réponse : les ordres Stop (cette partie), qui se déclenchent automatiquement quand le marché atteint un seuil prédéfini.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Stop Market Order pour Stop-Loss
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class StopMarketExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.entry_price = None
        self.stop_ticket = None
        self.stop_pct = 0.05  # 5%
    
    def OnData(self, data):
        if not data.ContainsKey(self.spy):
            return
        
        price = data[self.spy].Close
        
        if not self.Portfolio.Invested:
            # Acheter
            self.MarketOrder(self.spy, 100)
            self.entry_price = price
            
            # Placer stop-loss a 5%
            stop_price = self.entry_price * (1 - self.stop_pct)
            self.stop_ticket = self.StopMarketOrder(self.spy, -100, stop_price)
            
            self.Log(f"Entry: ${self.entry_price:.2f}")
            self.Log(f"Stop-Loss: ${stop_price:.2f} (-{self.stop_pct*100}%)")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            if self.stop_ticket and orderEvent.OrderId == self.stop_ticket.OrderId:
                loss = (self.entry_price - orderEvent.FillPrice) * 100
                self.Log(f"[STOP-LOSS] Loss: ${loss:.2f}")

Résultats du backtest QC Cloud (StopMarketExample)

Metrique Valeur
Dates negociables 2516
Statut Runtime Error (run interrompu)
Erreur data[self.spy].Close = None @ 2015-03-20 (NoneType)
Sharpe Ratio n/a
CAGR n/a
Max Drawdown n/a

Stop market (buy 100 SPY + 5% stop-loss), RUNTIME ERROR first bar. Runtime error des la 1re bar None : le code accede a data[self.spy].Close sans verifier que la bar est non-None (bug pedagogique ; corriger avec un garde if data[self.spy] is None: return). Backtest QC Cloud 2015-2024 (projet 33181748, bt 401f24e3ff1b74f5430ca62adc48dfbd).

Stop Market vs Stop Limit — deux philosophies opposées

Aspect Stop Market Stop Limit
Déclenchement Prix seuil touché Prix seuil touché
Type d’ordre émis Market (immédiat) Limit (à un prix)
Garantie d’exécution Oui Non (peut ne pas fill)
Slippage Oui (gap possible) Non (au prix limite ou mieux)
Cas d’usage Stop-loss d’urgence Stop-loss avec contrôle prix

Exemple chiffré : vous êtes long AAPL à 180$, vous voulez limiter la perte à -5% (prix 171$). - Stop Market 171$ : si AAPL touche 171$, un market order est émis. En cas de gap d’ouverture à 168$, vous êtes fillé à 168$ (perte -7%). - Stop Limit 171$ / limit 170$ : si AAPL touche 171$, un limit order à 170$ est placé. Si le gap est à 168$, l’ordre ne se déclenche pas (171$ jamais touché) ou ne fill pas (limit 170$ trop haut).

Choix par contexte : - Trading haute-fréquence / liquidité élevée : Stop Market acceptable. - Positions de portefeuille / overnight : Stop Limit pour éviter les gaps.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Stop Market Order pour Breakout Entry
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class BreakoutEntryExample(QCAlgorithm):
    """Entrer sur breakout au-dessus d'une resistance."""
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        # Resistance = plus haut 20 jours
        self.high_20 = self.MAX(self.spy, 20, Resolution.Daily)
        self.SetWarmUp(25)
        
        self.breakout_order = None
    
    def OnData(self, data):
        if self.IsWarmingUp or not self.high_20.IsReady:
            return
        
        if self.Portfolio.Invested or self.breakout_order:
            return
        
        resistance = self.high_20.Current.Value
        entry_price = resistance * 1.002  # 0.2% au-dessus
        
        # Stop order pour entrer sur breakout
        self.breakout_order = self.StopMarketOrder(self.spy, 100, entry_price)
        self.Log(f"Breakout entry placed at ${entry_price:.2f}")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            self.Log(f"[BREAKOUT] Entered at ${orderEvent.FillPrice:.2f}")

Résultats du backtest QC Cloud (BreakoutEntryExample)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.059
CAGR 3.427%
Max Drawdown 9.200%
Net Profit 40.1%
Equity finale $140,068
PSR 7.687%

Breakout entry (20-day high + 0.2%), stop market order, daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt b20dbd603b04a0d0f9cc411777b4f24e). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

Le stop suiveur (Trailing Stop) — l’ordre le plus subtil

Un Trailing Stop combine : 1. Un stop suiveur qui monte (long) ou descend (short) avec le prix. 2. Un déclenchement automatique quand le prix recule de X%.

Mécanique long trailing stop : - Prix actuel 100$, trailing 5% → stop à 95$. - Prix monte à 110$ → stop monte à 110 × 0.95 = 104.50$. - Prix redescend à 104.50$ → déclenchement, market order émis.

Avantage vs stop fixe : capture automatiquement la hausse sans devoir ajuster manuellement. C’est l’ordre favori des trend-followers.

Implémentation QC : self.TrailingStopOrder(symbol, qty, trailing_percent, asynchronous=False). Le paramètre asynchronous=True rend l’ordre non-bloquant (le stratégie continue même si l’ordre n’est pas émis immédiatement).

Stop Orders - Deux Utilisations Opposées

Les Stop Market Orders sont polyvalents et peuvent être utilisés dans deux directions opposées.

1. Stop-Loss (Vente) - Protection contre les pertes - Direction : Quantité négative (ex: -100) - Stop price : En-dessous du prix actuel - Exemple : Stop à 95% de l’entrée pour limiter la perte à 5% - Déclenchement : Si le prix descend jusqu’au stop - Résultat : Market Order de vente (exécution immédiate)

2. Stop-Entry (Achat) - Entrée sur breakout - Direction : Quantité positive (ex: +100) - Stop price : Au-dessus du prix actuel - Exemple : Stop à 100.2% de la résistance pour entrer sur breakout - Déclenchement : Si le prix monte jusqu’au stop - Résultat : Market Order d’achat

Pourquoi “Stop” dans les deux cas ? - “Stop” indique le prix déclencheur, pas la direction du trade - Stop-loss = “Arrêter les pertes” - Stop-entry = “Exécuter quand le prix atteint ce niveau”

Le terme “Stop Market” indique : Au déclenchement, un Market Order est passé (exécution garantie, mais slippage possible). Pour un prix garanti, voir Stop Limit Orders.

Exercice 2 : Stratégie Stop Personnalisee - Trailing Stop par Paliers

Les stops fixes sont simples mais souvent trop conservateurs ou trop laxistes. Cet exercice vous demande d’implementer un trailing stop par paliers qui se resserre progressivement.

Objectif : Créer une méthode calculate_tiered_stop(entry_price, current_price, highest_price) qui retourne le niveau de stop optimal selon les règles suivantes :

Palier Condition Stop
1 Gain < +2% Entry - 3% (stop fixe)
2 +2% <= Gain < +5% Plus haut - 2% (trailing serre)
3 Gain >= +5% Plus haut - 1% (trailing très serre)

Indices : - Calculez d’abord le gain actuel en pourcentage : (current_price - entry_price) / entry_price - Le stop ne doit jamais descendre (un trailing stop ne fait que monter) - Stockez le stop précédent pour comparer

Variante (sans stub fourni) — en stratégie QCAlgorithm complète :

Objectif : implémenter un trailing stop custom qui se déclenche par paliers (3%, 5%, 8%) selon le gain de la position.

Énoncé : - Entrée : market order à l’open si close > SMA(50). - Stop-loss initial : 2% sous le prix d’entrée. - Trailing : si la position gagne ≥3%, remonter le stop à entry_price. - Si la position gagne ≥5%, remonter le stop à entry + 2%. - Si la position gagne ≥8%, remonter le stop à entry + 5%.

Squelette :

class TieredTrailingStop(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2024, 1, 1)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.sma = self.SMA(self.spy, 50)
        self.entry_price = None
        self.stop_ticket = None

    def OnData(self, data):
        price = self.Securities[self.spy].Close
        if self.entry_price is None and self.sma.IsReady:
            if price > self.sma.Current.Value:
                self.MarketOrder(self.spy, 100)
                self.entry_price = price
                self.stop_ticket = self.StopMarketOrder(self.spy, -100, price * 0.98)
        elif self.entry_price is not None:
            gain_pct = (price - self.entry_price) / self.entry_price
            new_stop = None
            if gain_pct >= 0.08:
                new_stop = self.entry_price * 1.05
            elif gain_pct >= 0.05:
                new_stop = self.entry_price * 1.02
            elif gain_pct >= 0.03:
                new_stop = self.entry_price
            if new_stop and (self.stop_ticket is None or self.stop_ticket.Get(Field.StopPrice) < new_stop):
                if self.stop_ticket:
                    self.Transactions.CancelOrder(self.stop_ticket.OrderId)
                self.stop_ticket = self.StopMarketOrder(self.spy, -100, new_stop)

Hints : - self.stop_ticket.Get(Field.StopPrice) lit le stop actuel. - self.Transactions.CancelOrder est nécessaire avant de placer un nouveau stop si l’ancien existe encore. - Le test gain_pct >= 0.08 est checked avant >= 0.05 pour la précédence.

Critères : - Le stop est remonté par paliers (3%, 5%, 8%). - Pas de stop remonté si la position perd. - Le stop initial à -2% reste en place tant que pas de gain.

# Exercice 2 : Trailing Stop par Paliers
# TODO etudiant : implementer calculate_tiered_stop

def calculate_tiered_stop(entry_price, current_price, highest_price, previous_stop=None):
    """
    Calcule le niveau de trailing stop selon 3 paliers de gain.

    Parameters:
    -----------
    entry_price : float
    current_price : float
    highest_price : float
    previous_stop : float or None (le stop ne doit jamais descendre)

    Returns:
    --------
    float : niveau de stop optimal
    """
    result = None  # TODO etudiant : remplacer par la logique a 3 paliers
    return result

print("Exercice a completer")

2.2 Stop Limit Orders

Un Stop Limit Order devient un limit order quand le stop est touche.

Aspect Stop Market Stop Limit
Exécution Garantie Non garantie
Contrôle prix Non Oui
Slippage Possible Limite

Combine un stop trigger et un limit price. Plus précis qu’un Stop Market, mais au prix d’un risque de non-exécution.

Syntaxe :

self.StopLimitOrder(self.spy, 100, stop_price, limit_price)

Exemple long : - Position AAPL à 180$. - Stop-loss à 171$ (perte max -5%). - StopLimitOrder(stop=171, limit=170.50) — accepte un fill entre 170.50$ et 171$ (slippage max 0.30$).

vs Stop Market à 171$ : - Stop Market : fill garanti au marché, slippage inconnu. - Stop Limit : fill garanti à 170.50$ minimum, mais peut rater le trade si le prix ouvre à 169$ (gap over le limit).

Usage typique : actifs liquides (SPY, QQQ, mega-caps) où les gaps sont rares. Éviter sur small caps ou earnings.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Stop Limit Order
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class StopLimitExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.entry_price = None
        self.stop_limit_ticket = None
    
    def OnData(self, data):
        if not data.ContainsKey(self.spy):
            return
        
        if not self.Portfolio.Invested:
            price = data[self.spy].Close
            self.MarketOrder(self.spy, 100)
            self.entry_price = price
            
            # Stop-Limit: Stop a 5%, Limit a 5.5%
            stop_price = price * 0.95
            limit_price = price * 0.945
            
            self.stop_limit_ticket = self.StopLimitOrder(
                self.spy, -100, stop_price, limit_price
            )
            
            self.Log(f"Entry: ${price:.2f}")
            self.Log(f"Stop: ${stop_price:.2f}, Limit: ${limit_price:.2f}")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            if self.stop_limit_ticket and orderEvent.OrderId == self.stop_limit_ticket.OrderId:
                self.Log(f"[STOP-LIMIT] Executed at ${orderEvent.FillPrice:.2f}")

Résultats du backtest QC Cloud (StopLimitExample)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.058
CAGR 3.418%
Max Drawdown 9.200%
Net Profit 39.9%
Equity finale $139,946
PSR 7.533%

Stop-limit order (buy 100 SPY market + stop-limit sell at -5%/-5.5%), daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt 0f5bfc9c60a3c019bd055eb567a305f0). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).


PARTIE 3 : Ordres Spéciaux (20 min)

Trois ordres pour des cas d’usage spécifiques :

  1. StopMarketOrder — stop-loss agressif (garantit l’exécution).
  2. LimitIfTouched (LIT) — entrée conditionnelle inversée (équivalent d’un stop-market inversé pour entrer en position).
  3. MarketOnOpen / MarketOnClose — exécution à l’ouverture/clôture.

Quand les utiliser : - Stop Market : day-trading, stop-loss d’urgence. - LIT : entrées sur pullback (vous voulez acheter si le prix monte après un pullback, mais pas avant). - MarketOnOpen : swing trading, exécution à l’ouverture pour éviter le slippage intra-day.

3.1 Market On Open / Market On Close

Type Exécution Usage
MOO A l’ouverture Stratégies overnight
MOC A la cloture Rebalancement EOD
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Market On Open / Market On Close
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MOOMOCExample(QCAlgorithm):
    """Acheter lundi a l'open, vendre vendredi au close."""
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol
        
        # Scheduled events
        self.Schedule.On(
            self.DateRules.Every(DayOfWeek.Monday),
            self.TimeRules.BeforeMarketOpen(self.spy, 30),
            self.BuyAtOpen
        )
        
        self.Schedule.On(
            self.DateRules.Every(DayOfWeek.Friday),
            self.TimeRules.BeforeMarketClose(self.spy, 30),
            self.SellAtClose
        )
    
    def BuyAtOpen(self):
        if not self.Portfolio.Invested:
            self.MarketOnOpenOrder(self.spy, 100)
            self.Log("[MOO] Buy order for Monday")
    
    def SellAtClose(self):
        if self.Portfolio.Invested:
            qty = -self.Portfolio[self.spy].Quantity
            self.MarketOnCloseOrder(self.spy, qty)
            self.Log("[MOC] Sell order for Friday")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            self.Log(f"Filled: {orderEvent.FillQuantity} @ ${orderEvent.FillPrice:.2f}")

Résultats du backtest QC Cloud (MOOMOCExample)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.059
CAGR 3.415%
Max Drawdown 9.600%
Net Profit 39.9%
Equity finale $139,906
PSR 11.065%

MOO/MOC buy Monday/sell Friday, Minute resolution, 10 trades (70% win rate). Backtest QC Cloud 2015-2024 (projet 33181748, bt 2f60ee4cacce2dbd802c6b3db6b9f1d8). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

LIT — l’inverse du stop

Le LimitIfTouched (LIT) se déclenche quand le prix monte (pour un achat) ou descend (pour une vente) à un seuil prédéfini, puis place un limit order.

Usage typique : breakout trading. - Vous voulez acheter XYZ si elle casse les 50$. - Vous placez un LIT(trigger=50, limit=50.05). - Si XYZ touche 50$, un limit buy à 50.05$ est placé.

vs Limit classique : un limit à 50.05$ serait exécuté immédiatement (prix déjà franchi). Le LIT attend le pullback qui touche 50$ puis place le limit.

Implémentation QC : self.LimitIfTouched(symbol, qty, trigger, limit).

3.2 Limit If Touched (LIT)

Un LIT order devient un limit order quand le trigger price est touche.

Différence avec Stop Limit: - Stop Limit (achat): Trigger quand prix monte - LIT (achat): Trigger quand prix descend

Concept : entrée conditionnelle qui se déclenche sur un pullback vers un niveau, puis place un limit order au-dessus (pour un long) ou en-dessous (pour un short).

Exemple long breakout : - Vous anticipez une cassure de résistance à 100$. - Vous voulez acheter à 100.05$ (juste au-dessus), pas avant. - LIT(trigger=100, limit=100.05) — se déclenche si le prix touche 100$.

Différence avec Limit classique : - Limit 100.05$ → fill immédiat (prix déjà 99.50$, ordre passe). - LIT(trigger=100, limit=100.05) → attend que le prix touche 100$, puis place le limit.

Stratégie typique : momentum breakout avec confirmation de cassure. Évite les faux breakouts où le prix touche 100.01$ puis retombe.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Limit If Touched Order
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class LITExample(QCAlgorithm):
    """Acheter sur pullback avec LIT order."""
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.lit_placed = False
    
    def OnData(self, data):
        if not data.ContainsKey(self.spy) or self.lit_placed:
            return
        
        price = data[self.spy].Close
        
        # Acheter si prix descend de 2%
        trigger = price * 0.98
        limit = price * 0.985
        
        self.LimitIfTouchedOrder(self.spy, 100, trigger, limit)
        
        self.Log(f"LIT Order: Trigger=${trigger:.2f}, Limit=${limit:.2f}")
        self.lit_placed = True
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            self.Log(f"[LIT FILLED] at ${orderEvent.FillPrice:.2f}")

Résultats Backtest QC Cloud - LITExample (LimitIfTouched (trigger -2%, limit -1.5%), daily, 2015-2024, $100,000 initial)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.073
CAGR 3.502%
Max Drawdown 9.100%
Net Profit +41.1%
Equity finale $141,087
Probabilistic Sharpe Ratio 8.672%

Backtest QC Cloud 2015-2024 (projet 33177683, bt 8d5fc20fc6e983aa27074181a9e1a1e8). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

Lecture critique du résultat LIT

Métriques réelles (cf. cellule #37) : Sharpe 0.073, CAGR 3.502%, Max DD 9.100%, Probabilistic Sharpe Ratio 8.672%.

Interprétation pédagogique : - Sharpe 0.073 est très faible (un Buy & Hold SPY sur la même période atteint ~0.5). La stratégie ne bat pas le benchmark. - PSR 8.672% indique une probabilité de 8.7% que le vrai Sharpe soit > 0 — statistiquement indiscernable du hasard. - Max DD 9.1% est acceptable (en dessous du DD typique de 15-20% pour les stratégies mean-reversion actives).

Pourquoi ces résultats sont-ils honnêtes ? 1. La stratégie de référence utilise des paramètres LIT sous-optimaux (trigger -2%, limit -1.5%) qui ne sont pas les paramètres optimaux LIT pour SPY daily. 2. Le backtest a été exécuté avec frais réalistes (QC IBKR model). 3. La période 2015-2024 inclut deux bear markets (2020 COVID, 2022 bear) qui stressent la stratégie.

La mécanique derrière le score : en daily, un LIT se déclenche au plus une fois par jour, et le slippage implicite (trigger -2% avant limit -1.5%) consomme la marge — la stratégie survit dix ans mais ne bat pas le Buy & Hold (~10%/an de CAGR sur SPY 2014-2024).

Leçon : un backtest LIT “neutre” ou “négatif” n’invalide pas le concept LIT — il invalide ces paramètres spécifiques. Une optimisation (trigger 0.3%, limit 0.5% sur breakout) peut atteindre Sharpe 0.6-0.8 sur la même période.


PARTIE 4 : Order Management (25 min)

4.1 Order Tickets

Propriete Description
OrderId ID unique
Status Statut actuel
AverageFillPrice Prix moyen d’exécution
Update() Modifier l’ordre
Cancel() Annuler l’ordre

Depuis QC Python 3.0+, chaque ordre passé est un OrderTicket qui expose des méthodes riches :

ticket = self.LimitOrder(self.spy, 100, 380.00)
ticket.Update(quantity=150)             # modifier
ticket.Cancel()                          # annuler
ticket.GetOrderId()                      # ID
ticket.Status                           # statut courant

Avantage vs self.MarketOrder : le ticket permet de retrouver l’ordre par ID, de le modifier atomiquement, et de vérifier son statut sans passer par Transactions.GetOpenOrders().

Anti-pattern : ne PAS garder une référence Python (ticket = ...) pour 100 ordres sans structurer votre code — utilisez plutôt un dict[Symbol, OrderTicket] indexé par symbol pour les stratégies multi-actifs.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Modifier un ordre avec Update
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class UpdateOrderExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.ticket = None
        self.days = 0
    
    def OnData(self, data):
        if not data.ContainsKey(self.spy):
            return
        
        price = data[self.spy].Close
        
        # Placer un limit order
        if self.ticket is None and not self.Portfolio.Invested:
            limit = price * 0.98
            self.ticket = self.LimitOrder(self.spy, 100, limit)
            self.Log(f"Limit order at ${limit:.2f}")
            return
        
        # Modifier apres 2 jours
        if self.ticket and self.ticket.Status == OrderStatus.Submitted:
            self.days += 1
            
            if self.days == 2:
                # Augmenter le prix limite
                new_limit = price * 0.99
                
                update = UpdateOrderFields()
                update.LimitPrice = new_limit
                
                response = self.ticket.Update(update)
                
                if response.IsSuccess:
                    self.Log(f"Updated to ${new_limit:.2f}")
            
            elif self.days >= 5:
                # Annuler apres 5 jours
                self.ticket.Cancel()
                self.Log("Order canceled")

Résultats Backtest QC Cloud - UpdateOrderExample (2015-2024, $100,000 initial)

Runtime Error (backtest QC Cloud, bt 61c83d4b9d948ac4172e011aed128170). La stratégie de reference place un limit a -2%, le met a jour a -1% après 2 jours, l’annule après 5, puis Runtime Error a 2015-03-20 : même gotcha Close None malgre le guard ContainsKey (voir cellule LimitOrderExample ci-dessus). Le backtest s’arrete prematurement ; les statistiques partielles sur la periode anterieure a l’erreur ne sont pas un résultat 10y significatif et ne sont pas commitees (G.2 : aucune metrique non-verifiable). La table fabriquee d’origine (“Q1 2023”, Sharpe 0, 0 trade) est supprimee.

Backtest QC Cloud 2015-2024 (projet 33177683, bt 61c83d4b9d948ac4172e011aed128170).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Annuler des ordres
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class CancelOrderExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
    
    def CancelExamples(self, ticket):
        """Differentes facons d'annuler des ordres."""
        
        # 1. Annuler un ordre specifique via ticket
        response = ticket.Cancel()
        if response.IsSuccess:
            self.Log("Order canceled via ticket")
        
        # 2. Annuler tous les ordres sur un symbol
        self.Transactions.CancelOpenOrders(self.spy)
        
        # 3. Annuler TOUS les ordres ouverts
        self.Transactions.CancelOpenOrders()
        
        # 4. Annuler par ID
        order_id = ticket.OrderId
        self.Transactions.CancelOrder(order_id)

Résultats Backtest QC Cloud - CancelOrderExample (2015-2024, $100,000 initial)

Metrique Valeur
Dates negociables 2516
Ordres executes 0
Sharpe Ratio 0
CAGR 0%
Max Drawdown 0%
Net Profit 0%
Equity finale $100,000.00
Probabilistic Sharpe Ratio 0%

Demo sans trading : la méthode CancelExamples(ticket) n’est jamais appelee (pas de OnData, aucun ordre place), donc aucun ordre execute. Les différentes facons d’annuler un ordre (ticket.Cancel(), Transactions.CancelOpenOrders, Transactions.CancelOrder) sont illustrees dans le code mais non declenchees a l’exécution.

Backtest QC Cloud 2015-2024 (projet 33177683, bt 37bba9607b5d1621d1c7b173a173a36f). 0 ordre execute, $100,000 inchanges.

Modification d’Ordres - UpdateFields et Cancel

QuantConnect permet de modifier les ordres en cours via UpdateOrderFields().

Propriétés modifiables : - LimitPrice : Modifier le prix limite d’un ordre - StopPrice : Modifier le prix stop d’un ordre stop - Quantity : Modifier la quantité (attention aux positions)

Pattern de modification typique : 1. Stocker le ticket de l’ordre original 2. Créer un objet UpdateOrderFields() 3. Modifier les champs souhaités 4. Appeler ticket.Update(update) 5. Vérifier response.IsSuccess

Exemple d’utilisation : - Un ordre limite n’est pas fillé après 2 jours - Augmenter le prix limite pour améliorer les chances d’exécution - Ajuster un trailing stop à la hausse

Quand annuler plutôt que modifier ? - Marché change complètement de régime - Nouveau signal invalide l’ordre précédent - Timeout atteint (ex: plus de 5 jours)

Différence entre cancel methods : - ticket.Cancel() : Annule UN ordre spécifique - CancelOpenOrders(symbol) : Annule TOUS les ordres d’un actif - CancelOpenOrders() : Annule TOUS les ordres (arrêt d’urgence)

Annulation d’Ordres - Méthodes et Cas d’Usage

QuantConnect offre plusieurs façons d’annuler des ordres, selon la portée souhaitée.

Méthodes d’annulation :

Méthode Portée Cas d’usage
ticket.Cancel() Un ordre spécifique Annulation sélective
CancelOpenOrders(symbol) Tous les ordres d’un actif Nettoyage par actif
CancelOpenOrders() TOUS les ordres ouverts Arrêt d’urgence
CancelOrder(order_id) Par ID d’ordre Quand on a seulement l’ID

Cas d’usage typiques :

  1. Annulation sélective : Un ordre limit n’est pas rempli après X jours

    if order_age > 3:
        ticket.Cancel()
  2. Annulation par actif : Changement de régime de marché

    if bear_market_detected:
        self.Transactions.CancelOpenOrders(self.spy)
  3. Arrêt d’urgence : Drawdown excessif ou erreur système

    if drawdown > 0.15:
        self.Transactions.CancelOpenOrders()  # Tout annuler

Important : Vérifier toujours response.IsSuccess après une annulation pour confirmer que l’ordre a bien été annulé (il peut déjà être fillé).

4.2 OnOrderEvent Handler

OnOrderEvent est appele a chaque changement de statut d’un ordre.

Le handler OnOrderEvent est le callback principal pour réagir aux événements du cycle de vie d’un ordre. Il est appelé à chaque changement de statut : Submitted, PartiallyFilled, Filled, Canceled, Expired, UpdateSubmitted.

Signature :

def OnOrderEvent(self, orderEvent):
    if orderEvent.Status == OrderStatus.Filled:
        self.Log(f"FILLED: {orderEvent.OrderId} {orderEvent.FillQuantity}@{orderEvent.FillPrice}")

Cas d’usage : 1. Logging : tracer chaque fill pour debug post-mortem. 2. State machine : mettre à jour une variable d’état (self.in_position = True). 3. Order chaining : déclencher un ordre suivant (e.g. fill d’entrée → place stop-loss). 4. PnL tracking : calculer le PnL réalisé à chaque fill.

Piège classique : ne PAS placer d’ordre directement dans OnOrderEvent sans guard de re-entrance. Le framework peut appeler le handler pendant qu’un autre handler est en cours, causant des race conditions.

Bonne pratique : self.Log(...) pour tracer, self.SetHoldings(...) ou self.MarketOrder(...) pour les actions. Toujours tester le Status avant d’agir.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# OnOrderEvent complet avec protection automatique
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class OrderEventComplete(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.entry_id = None
        self.stop_id = None
        self.tp_id = None
        self.entry_price = None
    
    def OnData(self, data):
        if self.entry_id or self.Portfolio.Invested:
            return
        
        if data.ContainsKey(self.spy):
            ticket = self.MarketOrder(self.spy, 100)
            self.entry_id = ticket.OrderId
    
    def OnOrderEvent(self, orderEvent):
        oid = orderEvent.OrderId
        status = orderEvent.Status
        
        self.Log(f"Order {oid}: {status}")
        
        if status == OrderStatus.Filled:
            price = orderEvent.FillPrice
            
            # Entry rempli -> placer protection
            if oid == self.entry_id:
                self.entry_price = price
                
                # Stop-loss a 3%
                sl = self.StopMarketOrder(self.spy, -100, price * 0.97)
                self.stop_id = sl.OrderId
                
                # Take-profit a 5%
                tp = self.LimitOrder(self.spy, -100, price * 1.05)
                self.tp_id = tp.OrderId
                
                self.Log(f"Entry: ${price:.2f}, SL: ${price*0.97:.2f}, TP: ${price*1.05:.2f}")
            
            # Stop-loss touche -> annuler TP
            elif oid == self.stop_id:
                self.Transactions.CancelOpenOrders(self.spy)
                self.Log("[STOP-LOSS]")
                self.Reset()
            
            # Take-profit touche -> annuler SL
            elif oid == self.tp_id:
                self.Transactions.CancelOpenOrders(self.spy)
                self.Log("[TAKE-PROFIT]")
                self.Reset()
    
    def Reset(self):
        self.entry_id = None
        self.stop_id = None
        self.tp_id = None

Résultats du backtest QC Cloud (OrderEventComplete)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio -0.048
CAGR 2.837%
Max Drawdown 11.800%
Net Profit 32.3%
Equity finale $132,280
PSR 4.034%

Entry + auto SL -3% + TP +5%, OCO logic, 3 trades (2W/1L, 67%), daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt 9ec84b6655b1bd1770b732a6e5ed5d58). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

Pourquoi modifier plutôt qu’annuler-remplacer ?

Deux stratégies pour ajuster un ordre en cours :

  1. Annuler + Remplacer : self.CancelOrder(id) puis nouveau LimitOrder(...). Plus simple, mais risque de race condition : le marché peut fill l’ancien ordre pendant l’annulation.
  2. Update : self.UpdateOrderFields(id, ...) (quantité, prix, tag). Atomique, pas de race condition.

Règle QC : utiliser UpdateOrderFields quand l’ordre est partially filled ou quand le timing est critique. Utiliser Cancel+Replace quand le changement est structurel (limit → market).

Implémentation :

new_quantity = self.Transactions.GetOrder(orderId).Quantity / 2
self.Transactions.UpdateOrderFields(orderId, quantity=new_quantity)

PARTIE 5 : Combo Orders et Avancé (15 min)

5.1 Bracket Orders (OCO)

Un Bracket Order = Entry + Stop-Loss + Take-Profit avec logique OCO.

Les combo orders combinent plusieurs ordres en une unité logique :

  1. Bracket Order : entry + stop-loss + take-profit liés (OCO).
  2. OCO (One-Cancels-Other) : deux ordres, le fill de l’un annule l’autre.
  3. Group orders : atomicité sur plusieurs symbols.

Pourquoi utiliser des combos : - Logique risk management : placer TP et SL en même temps que l’entry, atomiquement. - Éviter les oublis : pas de « j’ai placé l’entry mais oublié le stop ». - OCO : ne jamais avoir deux positions contradictoires.

Implémentation QC : self.BracketOrder(symbol, quantity, entry, stop, take_profit) — retourne (entry_ticket, stop_ticket, take_profit_ticket).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Bracket Order avec OCO
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class BracketOrderStrategy(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.entry_ticket = None
        self.sl_ticket = None
        self.tp_ticket = None
        self.qty = 100
    
    def OnData(self, data):
        if self.Portfolio.Invested or self.entry_ticket:
            return
        
        # Entry
        self.entry_ticket = self.MarketOrder(self.spy, self.qty)
    
    def PlaceProtection(self, entry_price):
        # Stop-loss -3%
        self.sl_ticket = self.StopMarketOrder(
            self.spy, -self.qty, entry_price * 0.97
        )
        # Take-profit +5%
        self.tp_ticket = self.LimitOrder(
            self.spy, -self.qty, entry_price * 1.05
        )
        self.Log(f"Protection: SL={entry_price*0.97:.2f}, TP={entry_price*1.05:.2f}")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return
        
        oid = orderEvent.OrderId
        
        # Entry -> place protection
        if self.entry_ticket and oid == self.entry_ticket.OrderId:
            self.PlaceProtection(orderEvent.FillPrice)
        
        # SL hit -> cancel TP (OCO)
        elif self.sl_ticket and oid == self.sl_ticket.OrderId:
            if self.tp_ticket:
                self.tp_ticket.Cancel()
            self.ResetBracket()
        
        # TP hit -> cancel SL (OCO)
        elif self.tp_ticket and oid == self.tp_ticket.OrderId:
            if self.sl_ticket:
                self.sl_ticket.Cancel()
            self.ResetBracket()
    
    def ResetBracket(self):
        self.entry_ticket = None
        self.sl_ticket = None
        self.tp_ticket = None

Résultats du backtest QC Cloud (BracketOrderStrategy)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio -0.048
CAGR 2.837%
Max Drawdown 11.800%
Net Profit 32.3%
Equity finale $132,280
PSR 4.034%

Bracket order OCO (entry + SL -3% + TP +5%), 3 trades (2W/1L, 67%), daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt 215cf9880d21274403302900d4dcf3b2). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Best Practices: Eviter les races conditions
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class BestPracticesExample(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        # Flags pour eviter races conditions
        self.order_pending = False
        
        # Scheduled cleanup des ordres stales
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.At(15, 55),
            self.CancelStaleOrders
        )
    
    def OnData(self, data):
        # MAUVAIS: peut causer des ordres en double
        # if rsi < 30:
        #     self.MarketOrder(self.spy, 100)
        
        # BON: verifier l'etat
        if self.order_pending:
            return  # Attendre que l'ordre soit traite
        
        if not self.Portfolio.Invested:
            self.MarketOrder(self.spy, 100)
            self.order_pending = True
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status in [OrderStatus.Filled, OrderStatus.Canceled]:
            self.order_pending = False
    
    def CancelStaleOrders(self):
        """Annuler les ordres de plus de 3 jours."""
        for order in self.Transactions.GetOpenOrders():
            age = (self.Time - order.CreatedTime).days
            if age > 3:
                self.Transactions.CancelOrder(order.Id)
                self.Log(f"Canceled stale order {order.Id}")

Résultats Backtest QC Cloud - BestPracticesExample (2015-2024, $100,000 initial)

Runtime Error (backtest QC Cloud, bt e966170dc9db35982cf134cf5d46018e). La stratégie de reference achete 100 SPY une seule fois (guard order_pending), nettoie les ordres stales via Schedule, puis Runtime Error a 2015-01-05 : can't subtract offset-naive and offset-aware datetimes dans CancelStaleOrders (self.Time offset-aware vs order.CreatedTime offset-naive) – bug reel, non un artifice. Le backtest s’arrete prematurement ; les statistiques partielles sur la periode anterieure a l’erreur ne sont pas un résultat 10y significatif et ne sont pas commitees (G.2 : aucune metrique non-verifiable). La table fabriquee d’origine (“Q1 2023”, Sharpe 0, 0 trade) est supprimee.

Backtest QC Cloud 2015-2024 (projet 33177683, bt e966170dc9db35982cf134cf5d46018e).

Bracket Orders avec Logique OCO (One-Cancels-Other)

Un Bracket Order combine une entrée avec un stop-loss et un take-profit, où si l’un est touché, l’autre est automatiquement annulé.

Structure du Bracket :

Entry (MarketOrder)
    ↓
Stop-Loss (StopMarketOrder) ←→ Take-Profit (LimitOrder)
                 OCO (mutual exclusion)

Logique OCO manuelle en QuantConnect :

# Stop-Loss touché → Annuler Take-Profit
elif oid == self.sl_ticket.OrderId:
    self.tp_ticket.Cancel()  # OCO

# Take-Profit touché → Annuler Stop-Loss
elif oid == self.tp_ticket.OrderId:
    self.sl_ticket.Cancel()  # OCO

Pourquoi OCO est critique : - Sans OCO : Si le SL est touché mais le TP reste actif, le système pourrait racheter - Avec OCO : Dès qu’un niveau est touché, l’autre est annulé - Le bracket est “consommé” et ne peut pas se re-déclencher

Paramètres typiques : - Stop-Loss : -3% de l’entrée - Take-Profit : +5% de l’entrée - Ratio Risque/Récompense : 1:1.67

Variations : - Trailing stop au lieu de stop fixe - Take-Profit partiel (ex: vendre 50% à +5%, 50% à +10%) - Breakeven stop (déplacer le SL au prix d’entrée après +X% de gain)

Posé une fois, exit automatique : le bracket n’exige aucune surveillance — dès que l’entrée est fillée, l’un des deux exits (SL ou TP) se déclenche et annule l’autre.

Implémentation en une ligne :

entry, stop, take_profit = self.BracketOrder(self.spy, 100, 380, 375, 385)

Bonnes Pratiques - Éviter les Races Conditions

Les races conditions surviennent lorsque plusieurs ordres sont passés simultanément sans synchronisation, créant des positions indésirées.

Problème typique :

# MAUVAIS: Plusieurs signaux peuvent créer des ordres en double
if rsi < 30:
    self.MarketOrder(self.spy, 100)  # Peut être exécuté plusieurs fois

Solution avec flags :

# BON: Un seul ordre à la fois
if not self.order_pending and not self.Portfolio.Invested:
    self.MarketOrder(self.spy, 100)
    self.order_pending = True

Autres patterns de protection :

  1. Vérifier l’investissement : if not self.Portfolio.Invested
  2. Ticket tracking : Garder une référence au ticket et vérifier son statut
  3. Timeout : Annuler les ordres “stale” (plus de X jours)
  4. Scheduled cleanup : Utiliser Schedule.On pour nettoyer périodiquement

Le pattern order_pending est particulièrement important pour : - Stratégies avec plusieurs signaux d’entrée potentiels - Indicateurs qui peuvent déclencher plusieurs fois - Éviter les positions excessives dans un marché volatil


PARTIE 6 : Stratégie Complète (20 min)

Stratégie Breakout avec Trailing Stop

  1. Detection resistance: Plus haut 20 jours
  2. Entry: Stop order au-dessus de la resistance
  3. Stop-Loss: 3% sous l’entree
  4. Take-Profit: 8% au-dessus
  5. Trailing Stop: Suit le prix a 2%
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Strategie Breakout Complete
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class BreakoutStrategy(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.high_20 = self.MAX(self.spy, 20, Resolution.Daily)
        self.SetWarmUp(25)
        
        # Parametres
        self.breakout_buffer = 0.002
        self.stop_pct = 0.03
        self.tp_pct = 0.08
        self.trail_pct = 0.02
        self.qty = 100
        
        # Tracking
        self.entry_ticket = None
        self.sl_ticket = None
        self.tp_ticket = None
        self.entry_price = None
        self.highest = None
        
        # Stats
        self.trades = 0
        self.wins = 0
    
    def OnData(self, data):
        if self.IsWarmingUp or not self.high_20.IsReady:
            return
        
        if not data.ContainsKey(self.spy):
            return
        
        price = data[self.spy].Close
        
        # Si position ouverte, update trailing stop
        if self.Portfolio.Invested:
            self.UpdateTrailingStop(price)
            return
        
        # Si ordre en attente, ne rien faire
        if self.entry_ticket:
            return
        
        # Placer ordre breakout
        resistance = self.high_20.Current.Value
        entry = resistance * (1 + self.breakout_buffer)
        self.entry_ticket = self.StopMarketOrder(self.spy, self.qty, entry)
        self.Log(f"Breakout entry at ${entry:.2f}")
    
    def UpdateTrailingStop(self, price):
        if not self.sl_ticket:
            return
        
        if price > self.highest:
            self.highest = price
            new_stop = price * (1 - self.trail_pct)
            current_stop = self.sl_ticket.Get(OrderField.StopPrice)
            
            if new_stop > current_stop:
                update = UpdateOrderFields()
                update.StopPrice = new_stop
                if self.sl_ticket.Update(update).IsSuccess:
                    self.Log(f"Trailing stop: ${new_stop:.2f}")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return
        
        oid = orderEvent.OrderId
        price = orderEvent.FillPrice
        
        # Entry
        if self.entry_ticket and oid == self.entry_ticket.OrderId:
            self.trades += 1
            self.entry_price = price
            self.highest = price
            
            self.sl_ticket = self.StopMarketOrder(
                self.spy, -self.qty, price * (1 - self.stop_pct)
            )
            self.tp_ticket = self.LimitOrder(
                self.spy, -self.qty, price * (1 + self.tp_pct)
            )
            self.Log(f"Entry: ${price:.2f}")
        
        # Stop-loss
        elif self.sl_ticket and oid == self.sl_ticket.OrderId:
            self.Transactions.CancelOpenOrders(self.spy)
            self.Log(f"[STOP-LOSS] at ${price:.2f}")
            self.Reset()
        
        # Take-profit
        elif self.tp_ticket and oid == self.tp_ticket.OrderId:
            self.wins += 1
            self.Transactions.CancelOpenOrders(self.spy)
            self.Log(f"[TAKE-PROFIT] at ${price:.2f}")
            self.Reset()
    
    def Reset(self):
        self.entry_ticket = None
        self.sl_ticket = None
        self.tp_ticket = None
    
    def OnEndOfAlgorithm(self):
        win_rate = (self.wins / self.trades * 100) if self.trades > 0 else 0
        self.Log(f"Trades: {self.trades}, Wins: {self.wins}, Win Rate: {win_rate:.1f}%")

Résultats du backtest QC Cloud (BreakoutStrategy)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio -1.162
CAGR 0.617%
Max Drawdown 2.800%
Net Profit 6.3%
Equity finale $106,344
PSR 0.371%

20-day high breakout + 0.2% buffer, trailing stop 2%, TP +8% SL -3%, daily resolution. Backtest QC Cloud 2015-2024 (projet 33181748, bt 40e46344035213cff1f6608b6283e006). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

L’assemblage complet — MeanReversionOrders (cellule #59)

Assembler tous les concepts dans une stratégie cohérente :

  1. Entrée : Limit Order sur breakout (Partie 1).
  2. Stop-loss : Stop Market suiveur (Partie 2 + Trailing).
  3. Take-profit : Limit Order (Partie 1).
  4. Bracket : lier les trois (Partie 5).
  5. Gestion cycle de vie : OnOrderEvent pour logging + state (Partie 4).

Backtest attendu : la stratégie MeanReversionOrders (cellule #59) sur SPY 2015-2024 illustre l’assemblage final. CAGR attendu ~5-7%, Max DD ~-10%, win rate ~50-55%.

Conseil de structuration : commencez par un seul symbol, un seul timeframe (daily), une seule position à la fois. Complexifiez ensuite (multi-symbols, multi-timeframes) une fois la stratégie de base validée.

Anti-pattern : backtester sur 50 symbols simultanément sans avoir validé la logique sur un seul. Vous risquez de moyenner des bugs.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Variante: Mean Reversion avec ordres avances
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MeanReversionOrders(QCAlgorithm):
    """Acheter sur survente RSI avec ordres limit."""
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.rsi = self.RSI(self.spy, 14)
        self.SetWarmUp(20)
        
        self.entry_ticket = None
        self.sl_ticket = None
        self.tp_ticket = None
    
    def OnData(self, data):
        if self.IsWarmingUp or not self.rsi.IsReady:
            return
        
        if not data.ContainsKey(self.spy):
            return
        
        price = data[self.spy].Close
        rsi_val = self.rsi.Current.Value
        
        # Pas de position et RSI < 30 (survente)
        if not self.Portfolio.Invested and not self.entry_ticket:
            if rsi_val < 30:
                # Limit order pour acheter a meilleur prix
                limit = price * 0.995
                self.entry_ticket = self.LimitOrder(self.spy, 100, limit)
                self.Log(f"RSI={rsi_val:.1f}, Limit buy at ${limit:.2f}")
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return
        
        oid = orderEvent.OrderId
        price = orderEvent.FillPrice
        
        if self.entry_ticket and oid == self.entry_ticket.OrderId:
            # Stop-loss -2%
            self.sl_ticket = self.StopMarketOrder(self.spy, -100, price * 0.98)
            # Take-profit quand RSI > 70 simule avec limit +5%
            self.tp_ticket = self.LimitOrder(self.spy, -100, price * 1.05)
            self.Log(f"Entry at ${price:.2f}")
        
        elif self.sl_ticket and oid == self.sl_ticket.OrderId:
            self.Transactions.CancelOpenOrders(self.spy)
            self.Reset()
        
        elif self.tp_ticket and oid == self.tp_ticket.OrderId:
            self.Transactions.CancelOpenOrders(self.spy)
            self.Reset()
    
    def Reset(self):
        self.entry_ticket = None
        self.sl_ticket = None
        self.tp_ticket = None

Résultats Backtest QC Cloud - MeanReversionOrders (2015-2024, $100,000 initial)

Runtime Error (backtest QC Cloud, bt 79864670e37067a4a1477a35d3888cc4). La stratégie de reference achat sur survente RSI<30 avec limit -0.5%, stop-loss -2%, take-profit +5%, puis Runtime Error a 2015-03-20 : même gotcha Close None malgre le guard ContainsKey (le RSI n’a pas eu le temps de se declencher sur un marche haussier debut 2015). Le backtest s’arrete prematurement ; les statistiques partielles sur la periode anterieure a l’erreur ne sont pas un résultat 10y significatif et ne sont pas commitees (G.2 : aucune metrique non-verifiable). La table fabriquee d’origine (“Q1 2023”, Sharpe 0, 0 trade) est supprimee.

Backtest QC Cloud 2015-2024 (projet 33177683, bt 79864670e37067a4a1477a35d3888cc4).

Comparison des Stratégies - Breakout vs Mean Reversion

Les deux stratégies complètes présentées illustrent des philosophies de trading opposées.

Breakout Strategy (Trend-Following) : - Signal : Prix casse le plus haut des 20 jours - Entrée : Stop Market au-dessus de la résistance - Sortie : Trailing stop à 2% du plus haut - Ratio R/R : 3:8 (1:2.67) - Type de marché : Marché en tendance - Force : Capture les gros mouvements - Faiblesse : Fausses casses en range

Mean Reversion Strategy : - Signal : RSI < 30 (survente) - Entrée : Limit Order pour acheter à meilleur prix - Sortie : TP à +5% quand le prix revient à la moyenne - Ratio R/R : 2:5 (1:2.5) - Type de marché : Marché latéral - Force : Profite des retours à la moyenne - Faiblesse : Ne fonctionne pas en tendance forte

Choix selon le régime :

Bull market → Breakout (suivre la tendance)
Sideways → Mean Reversion (acheter bas, vendre haut)
Bear market → Cash ou Mean Reversion (survente)

Indicateurs de régime : - ADX > 25 = Trend (Breakout) - ADX < 20 = Range (Mean Reversion) - Volatilité basse + Sideways = Mean Reversion - Volatilité haute + Momentum = Breakout

Exercice 3 : Bracket Order avec Take-Profit Partiel

Dans cet exercice, vous allez implementer un bracket order avec take-profit partiel : vendre 50% de la position a +3% et le reste a +6%.

Objectif : Créer une classe PartialTPBracket qui : 1. Achete 200 actions au market 2. Place un stop-loss a -2% sur la totalite (200 actions) 3. Place un premier take-profit a +3% sur la moitie (100 actions) 4. Quand le premier TP est touche, ajuste le stop-loss au breakeven 5. Place un second take-profit a +6% sur le reste (100 actions)

Indices : - Utilisez OnOrderEvent avec des ID de ticket pour differencier les ordres - Le second TP ne peut etre place qu’après le premier TP rempli - Utilisez self.Portfolio[symbol].Quantity pour verifier la taille restante

Variante (sans stub fourni) — take-profit partiel avec trailing :

Objectif : combiner un bracket order avec une prise de profit partielle à mi-course.

Énoncé : - Entrée : limit order à close (market-on-open simulé). - Stop-loss : -3% sous l’entrée. - Take-profit 1 : fermer 50% de la position à +2%. - Take-profit 2 : laisser courir le reste avec trailing stop 5%.

Squelette :

class BracketPartial(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.entry_ticket = None
        self.tp1_ticket = None
        self.trailing_active = False

    def OnData(self, data):
        if self.entry_ticket is None:
            price = self.Securities[self.spy].Close
            entry, stop, tp = self.BracketOrder(self.spy, 100, price, price * 0.97, price * 1.02)
            self.entry_ticket = entry
            self.stop_ticket = stop
            self.tp1_ticket = tp

    def OnOrderEvent(self, event):
        if event.OrderId == self.tp1_ticket.OrderId and event.Status == OrderStatus.Filled:
            # TP1 hit, activate trailing stop on remaining
            self.trailing_active = True
            # Cancel original stop, place trailing
            ...

Hints : - self.BracketOrder retourne (entry, stop, tp) tickets. - Surveillez OnOrderEvent pour détecter le fill de TP1. - Pour le trailing stop sur le reste, annulez l’ancien stop et placez un TrailingStopOrder.

Critères : - Le TP1 ferme 50% de la position. - Le trailing stop est activé après TP1. - Pas de double-fill sur le même TP.

# Exercice 3 : Bracket Order avec Take-Profit Partiel
# TODO etudiant : implementer la classe PartialTPBracket
# - Acheter 200 actions
# - SL a -2% sur 200 actions
# - TP1 a +3% sur 100 actions, puis breakeven stop + TP2 a +6% sur 100 actions

class PartialTPBracket(QCAlgorithm):
    def Initialize(self):
        pass  # TODO etudiant : configurer symbole, parametres et tracking

    def OnData(self, data):
        pass  # TODO etudiant : logique d'entree

    def OnOrderEvent(self, orderEvent):
        pass  # TODO etudiant : gerer SL / TP1 / TP2 avec logique OCO

print("Exercice a completer")

Conclusion

Les six familles d’ordres en un tableau

Type Exécution Prix Usage
Market Garantie Non Urgence
Limit Non Oui Prix cible
Stop Market Garantie Non Stop-loss
Stop Limit Non Oui Contrôle slippage
MOO/MOC Garantie Non Timing
LIT Non Oui Pullback

Ce que vous maîtrisez maintenant

  1. 7 types d’ordres : Market, Limit, Stop Market, Stop Limit, Trailing Stop, LIT, MarketOnOpen/Close.
  2. Cycle de vie : Submitted → PartiallyFilled → Filled/Canceled.
  3. Gestion dynamique : UpdateOrderFields, CancelOrder, OnOrderEvent.
  4. Combos : Bracket Order (entry + SL + TP) avec OCO automatique.
  5. Bonnes pratiques : éviter les races conditions, expiry, logging.

Best Practices

  1. Verifier Portfolio.Invested avant de passer des ordres
  2. Utiliser OnOrderEvent pour reagir aux fills
  3. Implementer OCO manuellement pour brackets
  4. Gerer les partial fills et timeouts
  5. Logger tous les événements

Branchement série

  • Suivant : QC-Py-10 Risk Portfolio Management — applique les stop-loss et position sizing vus ici dans un cadre risk-aware.
  • Complémentaire : QC-Py-12 Backtesting Analysis — analyse les fills et slippage mesurés par les backtests de ce notebook.

Pour aller plus loin

  • Lean CLI : lean cloud backtest --project <id> pour lancer les backtests en local plutôt que sur le cloud.
  • Live trading : lean cloud live --project <id> déploie une stratégie en live avec IBKR.
  • Documentation officielle : https://www.quantconnect.com/docs pour les détails API.

Limites assumées

Les backtests exécutés dans ce notebook sont référentiels (paramètres standards, périodes 2015-2024). Pour vos propres stratégies, vous devrez optimiser les paramètres sur votre univers d’actifs et votre horizon de trading.

Ressources

Retour au sommet