Blog de développement

BackTest de la TradoBulle sur CAC40

Dernière Modification le :
2000-01-01

...

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 (Breakthrough)

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 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
        )
Graphique de base d'une Trado bulle exemplaire: elle flotte, rejoint la bande de Bollinger, puis la M7 puis la M20...
Exemple_Tradobulle_graphique-de-base
Graphique de base d'une Trado bulle exemplaire: elle flotte, rejoint la bande de Bollinger, puis la M7 puis la M20...

XXX

					    
XXX