Backtester une stratégie de trading automatisé en Python

Et s'il était possible de savoir en moins d'une seconde si une stratégie de trading est gagnante, cela serait bien, vous ne trouvez pas ?

Imaginez avoir à votre disposition un outil capable d'évaluer la performance passée d'une stratégie, vous fournissant des insights précieux sans avoir à risquer un seul centime de votre capital. Aujourd'hui, nous allons plonger dans le monde fascinant du backtesting. Nous explorerons comment, avec quelques lignes de code Python, vous pouvez tester la viabilité de votre stratégie de trading, évaluer ses forces, ses faiblesses et être mieux préparé pour entrer avec confiance dans le monde volatile du trading. Vous êtes prêt ? Alors, allons-y !

La stratégie de trading à backtester

Aujourd'hui, nous allons backtester les résultats de l'une des stratégies de trading les plus connues : le croisement de moyennes mobiles.

Une moyenne mobile représente simplement la moyenne du prix sur les n dernières périodes.

Cette stratégie consiste à acheter lorsqu'une moyenne mobile à court terme (par exemple 50 périodes) croise à la hausse une moyenne mobile à long terme (par exemple 200 périodes). Inversement, un signal de vente est déclenché lorsque la moyenne mobile à court terme croise à la baisse la moyenne mobile à long terme.

croisement-moyenne-mobile.png

C'est une méthode éprouvée pour identifier les points d'entrée et de sortie potentiels basés sur les tendances historiques du marché. Grâce à la puissance de Python et la bibliothèque pandas, en seulement quelques lignes de code, nous allons pouvoir déterminer si cette stratégie a été rentable par le passé, nous offrant ainsi des insights précieux pour nos futurs investissements.

Backtester une stratégie de trading en Python

Charger les données historiques avec Pandas

Pour pouvoir simuler notre stratégie de trading, vous vous en doutez peut-être, mais il va falloir les données historiques de prix de l'actif sur lequel nous voulons backtester notre stratégie. C’est un élément fondamental, car c'est grâce à ces données précieuses que nous pouvons analyser et évaluer la performance de notre stratégie dans des conditions de marché réelles. Nous vous mettons à disposition via ce lien https://github.com/CryptoRobotFr/python-pour-la-finance/blob/main/BTC-USDT.csv le cours OHLCV du Bitcoin heure par heure dans un fichier CSV. Ce fichier contient toutes les informations cruciales, telles que le prix d'ouverture, le prix le plus haut, le prix le plus bas, le prix de clôture et le volume, pour chaque heure des dernières années.

Après avoir importé la librairie Pandas avec import pandas as pd , nous allons commencer par charger ce fichier dans un DataFrame Pandas comme nous l’avons déjà fait avec l’instruction pd.read_csv()

df = pd.read_csv('BTC-USDT.csv'

Maintenant, nous allons convertir la colonne date en datetime pour faciliter le traitement des dates et heures. Les dates sont actuellement au format unix (nombre de millisecondes écoulées depuis 1970).

df["date"] = pd.to_datetime(df["date"], unit="ms")
df = df.set_index(df["date"])
del df["date"]

Avec ces lignes, nous avons converti la colonne 'date' en objet datetime, puis nous avons défini cette colonne comme l'index de notre DataFrame. Nous avons également supprimé la colonne 'date' originale pour garder notre DataFrame propre.

Calculer nos indicateurs techniques

Pour exécuter notre stratégie, nous allons avoir besoin préalablement de calculer nos indicateurs techniques qui sont ici la moyenne mobile court terme et long terme.

Une moyenne mobile sera sur 50 périodes et l'autre sur 200 périodes. Nous allons par la suite les utiliser pour générer des signaux d'achat et de vente.

df['ma50'] = df['close'].rolling(50).mean()
df['ma200'] = df['close'].rolling(200).mean()

Ici, nous avons utilisé la méthode rolling() pour créer nos moyennes mobiles. La moyenne mobile sur 50 périodes sera plus réactive aux changements de prix que celle sur 200 périodes.

Générer nos signaux d’achat et de vente

Maintenant que nous avons nos indicateurs, nous allons créer des colonnes pour les signaux d'achat et de vente basés sur la relation entre ces deux moyennes mobiles.

df['buy_signal'] = False
df['sell_signal'] = False
df.loc[(df['ma50'] > df['ma200']), 'buy_signal'] = True
df.loc[(df['ma50'] < df['ma200']), 'sell_signal'] = True

Nous avons initialisé nos colonnes de signaux avec des valeurs False. Puis, nous avons utilisé la méthode loc[] pour remplir ces colonnes avec des valeurs True lorsque les conditions d'achat ou de vente sont remplies.

Lorsque la moyenne mobile 50 périodes est au-dessus de la moyenne mobile 200 périodes, alors nous avons un signal d’achat, inversement, nous avons un signal de vente.

Préparer le backtest sur le Bitcoin

Nous sommes désormais prêts à exécuter le backtest. Nous initialiserons notre balance et parcourrons le DataFrame pour exécuter les ordres d'achat et de vente en fonction de nos signaux.

Nous allons commencer par initialiser 3 variables :

balance = 1000
position = None
asset = "BTC"
  • La variable balance représente le montant en dollar de notre portefeuille initial (ici 1000$).
  • La variable position représente la “position” en cours, si nous réalisons un achat, nous ouvrons la position et lorsque nous vendrons, nous fermerons la position. Nous commençons sans position donc la variable est initialisée à None.
  • La variable asset nous servira simplement à des fins d’affichage.

Exécuter le backtest en Python

Nous sommes maintenant arrivés à une étape cruciale de notre parcours : l’exécution du backtest à l'aide d'une boucle qui parcourt nos données historiques et applique notre stratégie de trading. Accrochez-vous, car nous allons décomposer et décortiquer chaque ligne pour vous permettre de saisir toute la subtilité et la puissance de ce code.

for index, row in df.iterrows():

Ici, nous entamons une boucle qui va parcourir notre DataFrame, ligne par ligne, chaque ligne représentant une période horaire dans nos données historiques du Bitcoin.

Chaque row contient les données de trading pour une heure spécifique, et index représente l'index temporel de cette ligne.

    if position is None and row['buy_signal']:

Nous entrons dans une condition où nous vérifions deux choses. D’abord, si nous n’avons pas de position ouverte (c’est-à-dire si nous avons acheté du Bitcoin) et ensuite, si le signal d’achat est activé pour la période en cours.

        open_price = row['close']
        open_usd_size = balance
        position = {
            'open_price': open_price,
            'usd_size': open_usd_size,
        }

Si c’est le cas, nous ouvrons une position. Nous notons le prix d’ouverture, qui est le prix de clôture de la période en cours. Nous investissons toute notre balance disponible et enregistrons ces détails dans la variable position via un dictionnaire.

        print(f"{index} - Buy for {open_usd_size}$ of {asset} at {open_price}$")

Enfin, nous imprimons un résumé de l’achat, indiquant la date et l’heure, la taille de la position et le prix d’achat.

    elif position and row['sell_signal']:

Nous vérifions ensuite si nous avons une position ouverte et si le signal de vente est activé.

        close_price = row['close']
        trade_result = (close_price - position['open_price']) / position['open_price']
        balance = balance + trade_result * position['usd_size']
        position = None

Si la condition est vraie, nous récupérons le prix de clôture pour la période en question. Ensuite, nous calculons le résultat du trade en soustrayant le prix d’ouverture de notre position au prix de clôture, le tout divisé par le prix d’ouverture pour obtenir le rendement en pourcentage. Notre balance est ensuite mise à jour en ajoutant le gain ou la perte du trade. Enfin, nous clôturons la position en la réinitialisant à None.

print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")

Pour chaque vente, nous imprimons un résumé du trade, y compris la date et l'heure, la nouvelle balance et le prix auquel nous avons vendu l’actif.

Une fois ce code réalisé, nous pouvons exécuter notre backtest et enfin découvrir les résultats. Grâce aux print() dans le code, nous affichons chaque achat et chaque vente afin d’explorer l’exécution de la stratégie.

resultat-strategie-84611.png

Les résultats de la stratégie de trading

Notre backtest est maintenant fini et nous pouvons rajouter un print() dans le but d’afficher notre portefeuille final :

print(f"Final balance: {balance}$")

Final balance: 7847.2548834433455$

Si nous avions appliqué cette stratégie de trading du croisement des moyennes mobiles de la fin 2017 jusqu'à début juillet 2023, notre portefeuille aurait grimpé à un impressionnant 7847 dollar. Oui, vous avez bien lu. Nous aurions réalisé un bénéfice de 6847 dollar! Soit +684%!

Mais avant de nous emballer, il est crucial de nuancer ces résultats. Nous n'avons pas intégré les frais de transaction dans notre simulation. Comme vous le savez, dans le monde réel, chaque achat et vente s'accompagne de frais qui peuvent impacter significativement les rendements.

De plus, il est essentiel de mettre ces gains en perspective avec la performance globale du Bitcoin durant cette période. Une stratégie est d’autant plus impressionnante si elle surperforme l’actif sous-jacent, offrant un retour ajusté au risque attrayant.

Néanmoins, il est clair que cette stratégie aurait été rentable par le passé. Cependant, et c'est un avertissement que nous ne saurions trop répéter : le passé ne prédit pas le futur. Les marchés évoluent, et une stratégie qui était gagnante hier peut ne pas l'être demain.

Dans le prochain épisode, nous plongerons plus profondément dans l'analyse des résultats de cette stratégie. Nous examinerons les mesures clés et les techniques pour évaluer objectivement la performance, et discuterons des ajustements potentiels pour améliorer la rentabilité et réduire le risque. Restez branchés, le meilleur est encore à venir !

Structurer notre backtest avec une Classe et des Fonctions

Dans notre quête pour rendre notre code plus propre, efficace et réutilisable, l'introduction des classes et des fonctions devient une étape incontournable. Envisagez par exemple la classe BacktestCrossMA ci-dessus. Cette structure nous permet non seulement de rendre notre code plus lisible mais aussi de l'organiser de manière modulaire, facilitant ainsi sa maintenance et sa réutilisation.

class BacktestCrossMA:
    def __init__(self) -> None:
        self.df = pd.DataFrame()

    def load_data(self, path):
        self.df = pd.read_csv(path)
        self.df["date"] = pd.to_datetime(self.df["date"], unit="ms")
        self.df = self.df.set_index(self.df["date"])
        del self.df["date"]

    def populate_indicators(self):
        self.df['ma50'] = self.df['close'].rolling(50).mean()
        self.df['ma200'] = self.df['close'].rolling(200).mean()

    def populate_signals(self):
        self.df['buy_signal'] = False
        self.df['sell_signal'] = False
        self.df.loc[(self.df['ma50'] > self.df['ma200']), 'buy_signal'] = True
        self.df.loc[(self.df['ma50'] < self.df['ma200']), 'sell_signal'] = True

    def run_backtest(self):
        balance = 1000
        position = None
        asset = "BTC"

        for index, row in self.df.iterrows():

            if position is None and row['buy_signal']:
                open_price = row['close']
                position = {
                    'open_price': open_price,
                    'usd_size': balance,
                }
                print(f"{index} - Buy for {balance}$ of {asset} at {open_price}$")

            elif position and row['sell_signal']:
                close_price = row['close']
                trade_result = (close_price - position['open_price']) / position['open_price']
                balance = balance + trade_result * position['usd_size']
                position = None
                print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")
                
            
        
        print(f"Final balance: {balance}$")

bt = BacktestCrossMA()
bt.load_data('BTC-USDT.csv')
bt.populate_indicators()
bt.populate_signals()
bt.run_backtest()

La classe BacktestCrossMA encapsule toutes les étapes de notre backtesting, de la récupération des données à l’exécution du backtest. Elle commence par initialiser un DataFrame, puis chaque méthode de la classe est dédiée à une étape spécifique du processus. La méthode load_data() charge les données, populate_indicators() calcule les moyennes mobiles, et populate_signals() détermine les signaux d'achat et de vente. Enfin, run_backtest() exécute le backtest basé sur ces signaux.

En encapsulant ces étapes dans une classe, nous pouvons facilement réutiliser et adapter notre code pour d’autres actifs ou stratégies, rendant notre travail non seulement plus efficace mais aussi plus organisé et gérable. Cela démontre la beauté et l'efficacité de la programmation orientée objet, une compétence précieuse pour tout développeur travaillant dans le domaine de la finance quantitative.

Mise en pratique

Après avoir exploré la stratégie du croisement des moyennes mobiles, il est temps de mettre nos connaissances en pratique avec une nouvelle approche. Nous vous proposons de mettre en pratique votre apprentissage avec une nouvelle stratégie de trading à backtester basée sur des Bandes de Bollinger, un autre outil d’analyse technique populaire parmi les traders.

Pour mettre en œuvre cette stratégie, nous achetons lorsque le prix de clôture est au-dessus de la bande supérieure de Bollinger et vendons lorsque le prix repasse en dessous de la bande moyenne. Si vous n’êtes pas famillier avec le concept de bandes de Bollinger nous vous laissons vous renseigner via ce lien https://www.abcbourse.com/apprendre/11_lecon4.html

Pour débuter, il sera nécessaire de se familiariser avec le calcul et l'interprétation des Bandes de Bollinger. En combinant la théorie avec la pratique, nous aurons l’occasion de tester et d’affiner une nouvelle stratégie, amplifiant ainsi notre arsenal de trading et affinant nos compétences en programmation Python pour la finance.

Librairie utile pour le calcul d’indicateur technique

Si vous souhaitez aller plus loin en testant d’autres stratégies basées sur d’autres indicateurs, nous vous conseillons d’aller voir du côté de la librairie python TA qui regroupe de nombreux indicateurs techniques. Voici le lien de la documentation de la librairie https://technical-analysis-library-in-python.readthedocs.io/en/latest/.

Une fois installée avec pip install ta puis importé dans votre code avec import ta vous pourrez par exemple calculer le RSI, un autre indicateur technique de trading très connu.

df["RSI"] = ta.momentum.RSIIndicator(close=df["close"], window=14).rsi()

Un exemple de correction se trouve ici: https://github.com/RobotTraders/Python_For_Finance/blob/main/exercise_correction_chapter_9.ipynb. Bon courage et rendez vous à la prochaine séance dans laquelle nous découvrirons comment analyser au mieux les résultats de ces backtest.