BackTest de la TradoBulle sur CAC40 Dernière Modification le : 2000-01-01 Le ZIP des Ressources ... Qu'est ce qu'une Bulle aka. TradoBulle Le point de départ est de comprendre que des mouvements de marchés peuvent être exagérés, ouvrant la voie à une correction, ou à un mouvement majeur, vers des plus hauts (ou plus bas). Cela a une incidence 'catastrophique' sur l'ecosystème des turbos, offrant des opportunités de crise. L'exagération sur indice et celle sur valeur (action) est peut être différente. La seconde a attiré mon attention initiale. Comme TradoSaure a publié sur l'exagération hors normes (comprendre, hors Bollinger) il l'a nommée et caractérisée. Principalement, on ira dans sa formation payante mais pas chère du tout au vu de sa richesse. Lire et se former → Au fil de l'eau, on pourra noter des vidéos comme celle de Janvier 2025 qui m'a inspiré ce backtest: Vidéo → : Lors d'une recherche thématique YouTube a mis en avant: Vidéo → Le mieux est d'acquérir la formation ! Vidéo → La bulle se distingue de la forte chute banale par le fait qu'elle sort complètement de la Bollinger Il recommande de faire ses propres backtests. Sur PRT, à la main, cela devient vite lasssant, d'où l'idée de les faire en Python Bulle vs Sortie (Breakout) La définition de la TradoBulle : Vidéo → SET UP TRADOBULLE : Condition 1: attendre la clôture ; Condition 2: aucun contact avec la Bollinger inférieure Note: fonctionne généralement mieux à la hausse qu'à la baisse Important: à tester sur vos actifs préférés avant de trader. On trouve aussi beaucoup de référence à la sortie de Bollinger, qui est moins restrictive en terme de définition. La Bulle est une sous-espèce de la Sortie. Quand on définit et qu'on indexe, on voit que les bulles sont rares, et souvent isolées. Les Sorties sont,elles, plus fréquentes et vivent plutôt en grappes. Identifier et indexer les Bulles et Sorties On dispose de nos historiques. On les met à jour et on ajuste la profondeur (la date de départ) à ce qui est utilisé par TradoSaure: le 01/01/1988. ABC-Bourse nous a donné des valeurs reconstituées pour les decennies antérieures. On les utilise pour calculer des moyennes 'longues' (ex. M200) et commencer notre index sans décalage. On calcule les moyennes sur un fichier commencant début 1987 (valeurs reconstituées) et on introduit l'index apès avoir tronqué à 1988. On s'inspire de ce projet GitHub Lire →, , nos données étant bien au même format OHLC. A noter que dans notre série historique (ABC-Bourse), pour les premières années O=H et B=C. df = pd.read_csv(chemin_fichier_historique_depuis_1987_asc, encoding='utf-8') # On recharge le 1987! # Calculer les indicateurs et Ajouter la moyenne glissante à 7 jours, à 50 jours, 200 j df['20d ma'] = df['Close'].rolling(window=20).mean() df['20d sdev'] = df['Close'].rolling(window=20).std() df['upper band'] = df['20d ma'] + (2 * df['20d sdev']) df['lower band'] = df['20d ma'] - (2 * df['20d sdev']) df['7d ma'] = df['Close'].rolling(window=7).mean() df['50d ma'] = df['Close'].rolling(window=50).mean() df['200d ma'] = df['Close'].rolling(window=200).mean() # Arrondir les colonnes à 2 chiffres après la virgule df[['Open', 'High', 'Low', 'Close', '20d ma', '20d sdev', 'upper band', 'lower band','7d ma','50d ma','200d ma']] = df[['Open', 'High', 'Low', 'Close', '20d ma', '20d sdev', 'upper band', 'lower band','7d ma','50d ma','200d ma']].round(2) # Sauvegarder avec les colonnes arrondies et lire à nouveau le fichier m20_bollenger_1987 df.to_csv(chemin_fichier_m20_bollenger_1987, encoding='utf-8', index=False, columns=['Date', 'Open', 'High', 'Low', 'Close', '20d ma', '20d sdev', 'upper band', 'lower band','7d ma','50d ma','200d ma']) df = pd.read_csv(chemin_fichier_m20_bollenger_1987, encoding='utf-8') # Filtrer les dates postérieures à 1988, Sauvegarder le fichier SANS Index, puis le relire df = df[df['Date'] > 19880000] df.to_csv(chemin_fichier_m20_bollenger, encoding='utf-8', index=False, columns=['Date', 'Open', 'High', 'Low', 'Close', '20d ma', '20d sdev', 'upper band', 'lower band','7d ma','50d ma','200d ma']) df = pd.read_csv(chemin_fichier_m20_bollenger, encoding='utf-8') df.index.name = 'Seance' df['Bulle'] = '' # Ajouter la colonne Bulle avec des valeurs par défaut vides # Remplir la colonne Bulle selon les conditions df.loc[(df['Open'] > df['upper band']) & (df['High'] > df['upper band']) & (df['Low'] > df['upper band']) & (df['Close'] > df['upper band']), 'Bulle'] = 'H' df.loc[(df['Open'] < df['lower band']) & (df['High'] < df['lower band']) & (df['Low'] < df['lower band']) & (df['Close'] < df['lower band']), 'Bulle'] = 'B' # Ajouter la colonne Sortie df['Sortie'] = '' df.loc[df['Close'] > df['upper band'], 'Sortie'] = 'H' df.loc[df['Close'] < df['lower band'], 'Sortie'] = 'B' # Sauvegarder le fichier avec les nouvelles colonnes df.to_csv(chemin_fichier_m20_bollenger_m7_bulle,encoding='utf-8',index=True, index_label='Index',columns=['Date', 'Open', 'High', 'Low', 'Close', '20d ma', '20d sdev', 'upper band', 'lower band', 'Bulle', 'Sortie', '7d ma', '50d ma', '200d ma']) https://market.prorealcode.com/product/bollinger-bubble-screeners-pack/ https://www.youtube.com/embed/KVFy1BgQA9E https://www.youtube.com/watch?v=pOsyZoLu8to Da Approche vectorisée, globale Cet article https://dev.to/blankly/build-a-backtester-in-python-in-10-minutes-12je (avec PDF et traduction dans le ZIP de ressources) explique la différence entre l'approche vectotisée et l'approche step-by-step. En un mot, on le fait en Python mais on aurait aussi bien pu le faire en Ecxel ou LibreOffice. Nos valeurs (Moyennes, Bollinger) sont précalculées globalement dans un tableau. Les valeurs sont précalculées : ex: sma7, sma200. on les utilise ou pas... Indexation Je fais dans cet ordre: calcul des moyennes mobiles (y compris 1987), troncature au début 1988, construction de l'index commencant par zéro au 01-01-1988 Je construis l'index une fois les moyennes mobiles précalculées (y compris sur des valeurs de 1987) et la base tronquée au début 1988. Ainsi le tableau a bien des valeurs dans toutes les lignes et je n'ai pas à gérer les bords. Je documente une colonne Bulle avec un H ou un B selon que la valeur traverse la bande Basse ou la bande Haute. Pour la Bulle, les 4 valeurs OHLC doivent être sous (ou sur) la bande. Autant dire que H est sous la bande (resp. sur...). Pour la Sortie, on documente selon que la cloture (C) est hors bande. Une Bulle est un cas particulier de la Sortie. Les Sorties (breakout) sont souvent en grape. Les Bulles sont souvent isolées (tout en étant dans une séquence de sortie). On peut vouloir travailler sur les bulles, les sorties, les séquences, les sorties terminales, les séquences à pallier, etc. Les gens nomment ces evenements (en série) avec un vocabulaire qu'il serait bien d'utiliser avec rigueur: T1, T2, T3 Bulle : une seule ligne de code pour créer le registre sur valeurs précalculées! registre_df = df[df['Bulle'] != ''].copy() Sortie de Bollinger Ultime: Une séquence de sorties se termine quand la ligne suivante rentre dans le rang. Avec Pandas, osculter la valeur des lignes suivantes ou précédentes avec SHIFT df.loc[(df['Sortie'] != '') & (df['Sortie'].shift(-1) == ''), 'sortie_terminale'] = df['Sortie'] Petit inventaire : Total bulles: 120 dont Bulles Hautes: 64 et Bulles Basses: 56. Graphiques avec mplfinance J'ai mis un petit programme de test dans le ZIP de ressources: il génére des data et un graphique # Afficher le graphique OHLC avec mplfinance mpf.plot(data, type='candle', volume=False, style='charles', title='Données OHLC fictives', ylabel='Prix',savefig=save_path) Mes données historiques sont au format OHLC voulu, sauf les premières années, ce qui nous prive alors des mèches (ombres) Pour une introduction à MPLFINANCE Lire → Les couleurs standards sont : blue, red, green, orange, purple, brown, pink, gray, olive, cyan . Vous pouvez également utiliser des codes hexadécimaux (ex. "#FF5733") ou des notations RGB. Le graphique de base inclut les bandes de Bollinger (objectif #1), la M7D (objectif #2) , la M20D (objectif #3) for index, row in registre_df.iterrows(): # Trouver l'index de la date de la bulle dans le DataFrame principal bulle_index = df[df['Date'] == row['Date']].index[0] span_mini=10 span_maxi=span_mini + 10 # Définir les limites de la plage de données (10 séances avant et 20 après) start_date = max(bulle_index - span_mini, 0) # Ne pas aller en dessous de 0 end_date = min(bulle_index + span_maxi, len(df) - 1) # Ne pas dépasser la fin du DataFrame df_bulle = df.iloc[start_date:end_date + 1].copy() # Filtrer les données autour de la date de la bulle +1 pour inclure end_date # Créer le graphique en chandeliers japonais if not df_bulle.empty: df_bulle['Date'] = pd.to_datetime(df_bulle['Date'], format='%Y-%m-%d') # Assurez-vous que le format correspond à vos données df_bulle.set_index('Date', inplace=True) index_normalise = str(index).zfill(3) # Normaliser l'index sur 3 digits pour les noms de fichiers clean_date = row['Date'].strftime('%Y-%m-%d') # Format : YYYY-MM-DD (pour les noms de fichiers) date_titre = row['Date'].strftime('%d/%m/%Y') # Format : DD/MM/YYYY (pour les titres des graphiques) titre_bulle = 'Bulle DESSOUS Boll.' if row['Bulle'] == 'B' else 'Bulle DESSUS Boll.' titre = f"CAC40 # {index_normalise} {titre_bulle} : {date_titre}" # print(f"titre = {row['Bulle']} - Date {clean_date}") save_path = os.path.join(dossier_images, f"bulle_{index_normalise}_{clean_date}.jpg") # "blue, red, green, orange, purple, brown, pink, gray, olive, cyan" Vous pouvez également utiliser des codes hexadécimaux (ex. "#FF5733") ou des notations RGB 20d ma apds = [ mpf.make_addplot(df_bulle['upper band'], color='orange', width=1), mpf.make_addplot(df_bulle['lower band'], color='orange', width=1), mpf.make_addplot(df_bulle['7d ma'], color='purple', width=1), mpf.make_addplot(df_bulle['20d ma'], color='olive', width=1) ] fixed_offset = 100 # Valeur absolue fixe pour décaler un marker et bien identifier la bulle if row['Bulle'] == 'B': fixed_offset *= -1 bubble_marker = 'v' if row['Bulle'] == 'H' else '^' bubble_marker_series = pd.Series(data=np.nan, index=df_bulle.index) if row['Date'] in df_bulle.index: bubble_marker_series.loc[row['Date']] = df_bulle.loc[row['Date'], 'Close'] + fixed_offset apds.append(mpf.make_addplot(bubble_marker_series, type='scatter', markersize=150, marker=bubble_marker, color='orange')) mpf.plot( df_bulle, type='candle', # Type de graphique : chandeliers japonais style='charles', # Style du graphique title=titre, # Titre du graphique ylabel='', xlabel='', # Étiquette de l'axe Y ylabel='Prix' volume=False, # Ne pas afficher le volume addplot=apds, # Ajouter les Bollinger Bands show_nontrading=False, axisoff=True, savefig=save_path # Sauvegarder le graphique ) Le résultat... Graphique de base d'une Trado bulle exemplaire: elle flotte, rejoint la bande de Bollinger, puis la M7 puis la M20... Stop de protection, pouce vert ou rouge La méthode de base, décrite par le formateur Tradosaure sur Youtube, consiste valider par pouces verts ou rouges diverses hypothèses. Si votre hypothèse est que le CAC va monter sans vous fixer un cadre, vous enfoncez une porte ouverte. A la fin, le CAC monte toujours... Pour tester une hypothèse que le CAC, après être sorti de ses gonds, va revenir à la normale, il faut se limiter à une fenêtre temporelle. Le stop de protection définit 2 choses: votre mise initiale et la limite de temps. Les objectifs sont par exemple: - Objectif 1: toucher (ou enfoncer ?) la Bollinger (en définir les modalités ???) - Objectif 2: toucher la M7 (laquelle est mobile...) - Objectif 3: toucher la M20 (laquelle est mobile...) Si le stop est enfoncé: le test prend fin. Les outcomes possibles sont: Stop Objectif 1 puis retour sur le Stop Objectif 1, puis 2, puis retour sur le Stop Objectifs 1, 2 puis 3, puis retour sur le Stop (ou pas) Pour une bulle Haute, cela revient à dire que Low est inférieure à Bande Haute, M7, M20 qui sont mobiles, peuvent être calculée en mode tableur sur la table historique mais pas à l'instant t. Au moment de la prise de risque, le ratio Gain/Risque est inconnu... Si les objectifs étaient définis en tant que pourcentage (ex: 1 x R1, 2 x R1... ou 0,5 %, 1 % , etc. ou Bande Haute, M7 , M20 au jour de la bulle), une distribution de probabilité du ratio Gain/Risque pourrait être construite au jour de la prise de risque. Avec un script Python, on a le choix de ces objectifs. Si l'ojectif est fixé à priori, l'approche est vectorielle (un tableur suffit, sans boucle). Sinon, il faut une boucle et être attentif à la fin de boucle. Dans l'approche classique, les objectifs sont le retour dans la bande (Toute la bougie dans la bande ?), toucher la M7, toucher la M20: ils sont mobiles, d'où une approche pas-à-pas avec une boucle... Stop de protection, pouce vert ou rouge L'objectif peut être atteint selon plusieurs modalités: une des valeurs OHLC touche l'objectif, Close passe l'objectif, toutes les valeurs OHLC passent l'objectif Les types de validation sont : toucher, passer, passer-et-cloturer. La position initiale (H ou B) determine l'opérateur logique < ou >) Début et fin de boucle A priori, le test commence juste après la cloture, au jour 0. En fait, on pourrait ajouter 1 ou 2 jours d'attente... Le test est fini soit lorsque tous les objectifs sont atteints, soit lorsque le stop est déclanché. Le marqueur 'stop est déclanché' est défini en début de test. Un marqueur 'tous les objectifs sont atteints' est réévalué à chaque séance. Si la séance en cours est supérieure ou égale à 'stop est déclanché': le test prend fin (pour cette bulle). Si le marqueur 'tous les objectifs sont atteints' est vrai, alors le test (pour cette bulle) prend fin. Le backtest dans son ensemble est terminé lorsque chaque bilan de bulle est terminé, ou pour les tests non terminés (le trade est en court), la dernière séance est atteinte. https://latex.developpez.com/cours/savoir-latex-sans-oser-demander/?page=partie-1 https://matplotlib.org/stable/gallery/lines_bars_and_markers/marker_reference.html#markers-created-from-paths https://matplotlib.org/stable/api/markers_api.html#module-matplotlib.markers For using markers, these references may help: https://matplotlib.org/3.1.0/gallery/lines_bars_and_markers/marker_reference.html https://matplotlib.org/3.2.1/tutorials/text/mathtext.html (note sections on numbers and arrows) https://stackoverflow.com/questions/44726675/custom-markers-using-python-matplotlib https://stackoverflow.com/questions/14324270/matplotlib-custom-marker-symbol https://www.quantstart.com/articles/creating-an-algorithmic-trading-prototyping-environment-with-jupyter-notebooks-and-plotly/ https://github.com/matplotlib/mplfinance/issues/354 https://matplotlib.org/stable/gallery/color/named_colors.html XXX Previous Home Next