ProPython
Créer un Bot de Trading - Introduction
02 Apr, 2021
Difficile
Prérequis
Bases de PythonBase du tradingPandasBacktrader

Créer un Bot de Trading - Introduction

Aujourd'hui, il est tout à fait possible pour n'importe qui de créer un système de trading automatisé. En effet, avec les nombreux langages de programmation disponibles, ainsi que des API permettant de récupérer des données de marché historiques, on dispose de beaucoup d'outils. Encore faut-il savoir comment les utiliser.

Dans ce projet, nous allons donc apprendre à créer un bot de trading en utilisant le Python. Mais avant de rentrer dans le vif du sujet, il est nécessaire de définir ce qu'est un bot de trading, puis de décider des fonctionnalités à lui implémenter.

À noter que vous n'êtes pas obligés de suivre à la lettre ce que je propose et mes codes source, je pense qu'il est même mieux de créer quelque chose en s'inspirant de ce que je propose plutôt qu'en recopiant exactement, cependant vous êtes libres de faire ce que vous voulez, dans tous les cas mon programme sera disponible au téléchargement à la fin de ce projet ! Notez aussi que le bot que nous allons créer sera utilisé pour le trading de cryptomonnaies, mais le principe reste pareil peu importe le marché.

Définition

Un bot de trading, c'est un outil, automatisé ou semi automatisé qui permet de passer des ordres sur un marché financier. Mais ça peut également être un outil utilisé afin de récupérer des données financières historiques, pour pouvoir backtest une stratégie de trading, c'est à dire mesurer sa pertinence en utilisant des données passées.

Fonctionnalités

Voici les fonctionnalités que je souhaite intégrer à mon bot de trading :

Trading en live automatisé

C'est un peu la fonctionnalité principale d'un bot de trading. On souhaite réaliser un bot qui tourne 24H/24 en automatique. Attention, il tourne tout le temps mais cela ne veut pas dire qu'il est tout le temps actif. Typiquement, il ne sera pas actif quand le marché financier sur lequel il trade sera fermé.

On souhaite également que ce bot tourne avec une stratégie qu'on aura clairement défini. Et on aimerait pouvoir créer simplement des stratégies afin de changer comme bon nous semble celle qui tourne sur le bot, on souhaite donc beaucoup de flexibilité à ce niveau.

En bonus, on intégrera une fonctionnalité paper trading, qui permet de trader avec de l'argent fictif.

Backtesting

Il est bien de tester ses stratégies avant de les appliquer directement sur le marché. On souhaite donc réaliser une fonction nous permettant de tester une stratégie sur des données historiques. On programmera des indicateurs de performance nous permettant d'avoir des rapports détaillés à propos de la pertinence d'une stratégie.

On intégrera également une fonctionnalité pour visualiser graphiquement une stratégie et les données historiques.

Notifications Telegram

Afin de connaitre l'état du bot sans avoir à vérifier notre ordinateur tout le temps, on intégrera une fonctionnalité afin de recevoir des notifications Telegram sur notre téléphone à chaque action effectuée par le bot.

Intégration Raspberry Pi

Ceci est une fonctionnalité que vous n'utiliserez probablement pas, mais elle m'est utile à moi. Un Raspberry Pi est un mini ordinateur, on peut donc y faire fonctionner des programmes. Plutôt que de laisser allumé mon ordinateur tout le temps, une solution plus optimale est d'utiliser un Raspberry Pi, qui lui est conçu pour pouvoir fonctionner tout le temps.

Bonus : intégration MongoDB

MongoDB est un système de gestion de base de données. Il peut être utile de sauvegarder les données utilisées et générées par le bot dans une base de données. En revanche ce n'est pas vraiment une fonctionnalité essentielle, et elle ne sera pas très poussée.

Organisation

Maintenant que nous avons bien défini ce qu'on souhaite développer, il faut organiser le fonctionnement de notre bot en plusieurs étapes afin de bien séparer les étapes de développement :

  • Collecte de données financières
  • Analyse des données par la stratégie
  • Prise de décision
  • Création des ordres

Bien sûr ces étapes seront divisées en sous étapes, mais au moins nous avons en tête les grandes lignes. On peut maintenant commencer à coder !

Développement

Mise en place du projet

Dans votre IDE favori, créez un projet, et éventuellement un environnement virtuel pour votre interpréteur, puisque nous devrons installer des librairies.

À la racine de votre projet, créez un script test.py qu'on utilisera pour tester le développement de notre bot. Créez aussi un dossier models dans lequel on mettre tous nos objets, et un dossier data dans lequel on stockera les données générées par le programme. Maintenant nous allons pouvoir attaquer la partie pour extraire les données financières.

Extraction des données historiques

Pour extraire les données historiques, le plus simple est d'utiliser des API déjà existantes. On peut utiliser Yahoo Finance, ou bien les API des exchanges les plus populaires comme Binance, ou Kraken par exemple... Le mieux est de créer une classe pour s'occuper de l'extraction des données, et de définir cette classe comme abstraite pour qu'elle agisse comme une interface. Cela nous permet d'être très flexible et de créer par exemple une sous-classe pour extraire des données via Binance, une autre via Coinbase, une autre via Yahoo Finance, vous avez compris le principe. Dans notre cas, nous allons utiliser une librairie qui va nous simplifier le travail et nous n'aurons pas besoin de réaliser d'interface. Cependant, cette librairie ne prend en charge que le marché des cryptomonnaies, si vous faites un bot pour un autre marché il faudra faire autrement.

Installation de ccxtbt

La librairie à installer est ccxtbt. Pour l'installer, voici la commande à entrer dans votre interpréteur :

pip3 install git+git://github.com/Dave-Vallance/bt-ccxt-store

Installation de backtrader

La seconde librairie à installer est backtrader. Voici la commande :

pip3 install backtrader

Cependant, il faut légèrement modifier cette librairie car elle n'est pas à jour. Ouvrez donc le dossier de votre librairie se situant soit dans votre environnement virtuel, soit à l'endroit où pip installe les librairies par défaut. Ouvrez le dossier plot, puis locator.py, et supprimez warnings à la ligne 35 de ce fichier.

Création de la classe DatafeedGenerator

Nos données historiques extraites vont être utilisées par la librairie backtrader, sous forme de Datafeed, c'est à dire un objet représentant des données historiques, ou un flux de données. On crée donc une classe DatafeedGenerator dont l'objectif est de produire un Datafeed qui sera utilisé par le bot. Cette classe possède un attribut correspondant aux paramètres des données qu'on souhaite obtenir, par exemple la date de début des données historiques, la date de fin, l'intervalle de valeurs, etc... On modélise ces paramètres dans une classe à part, qu'on appelle DatafeedParams. Voici notre classe de paramètres :

from dataclasses import dataclass
import datetime as dt
import backtrader as bt
import ccxt


@dataclass
class DatafeedParams:
    mode: str
    symbol: str
    timeframe: bt.TimeFrame
    compression: int = 1
    end_date: dt.datetime or str = dt.datetime.utcnow()
    start_date: dt.datetime or str = None
    timedelta: dt.timedelta = None
    debug: bool = False
    exchange: any = ccxt.bitfinex()

Remarquez qu'on utilise une dataclass. Si vous ne savez pas ce que c'est : dataclasses en Python. Il y aura un article à ce propos de toute façon.

Voici à quoi correspondent les paramètres de notre classe :

  • mode : paramètre indiquant le mode dans lequel sera généré le Datafeed. Soit pour le mode live, soit pour le mode backtesting.
  • symbol : paire de devises que vous souhaitez utiliser (ex: BTC/EUR)
  • timeframe et compression : paramètres pour la résolution et l'intervalle des données (ex: 1min, 4h, 1j)
  • start/end date : dates de début et de fin de l'extraction des données
  • timedelta : paramètre optionel pour utiliser une différence de temps afin d'obtenir la date de début d'extraction (ex: 1semaine pour obtenir les données à partir d'1 semaine avant la date de fin
  • debug : pour obtenir des informations de debug sur le Datafeed
  • exchange : exchange à utiliser pour l'extraction des données.

Voilà ! On peut maintenant créer notre classe DatafeedGenerator et y inclure notre classe de paramètres :

class DatafeedGenerator:


    def __init__(self, datafeed_params):
        self.p = datafeed_params


        if type(self.p.start_date) == str:
            self.p.start_date = dt.datetime.strptime(self.p.start_date, "%Y/%m/%d %H:%M:%S")
        if type(self.p.end_date) == str:
            self.p.end_date = dt.datetime.strptime(self.p.end_date, "%Y/%m/%d %H:%M:%S")

        if self.p.timedelta:
            self.p.start_date = self.p.end_date - self.p.timedelta

Remarquez qu'on a rajouté des lignes de code dans le constructeur afin de traiter les cas où les dates seraient indiquées soit sous forme de chaînes de caractères, soit sous forme de datetimes et qu'on a également traité le cas où le paramètre timedelta est renseigné.

Maintenant, on va pouvoir traiter la génération des données.

Il faut distinguer le cas où on souhaite créer un flux de données, pour avoir des données en live s'actualisant automatiquement, ou des données statiques allant d'une date de début à une date de fin.

Commençons par nous occuper du cas statique.

Dans nos paramètres, on dispose d'un objet exchange, représentant un exchange (surprenant !), et on peut obtenir des données historiques sous forme OHLCV (Open High Low Close Volume) en utilisant la méthode fetch_ohlcv(symbol, timeframe, since, limit). On dispose déjà du symbol dans nos paramètres, et on met limit à 10 000 qui est la valeur max possible (limit correspond au nombre limite de données pouvant être extraites en une fois. Par exemple, si on extrait des données avec une résolution à la minute, on aura 1 donnée par minute, soit 60 par heure, soit 1440 par jour. Si on veut extraire les données à la minute des 10 derniers jours, on ne pourra pas puisqu'on aura besoin de 14 400 données et que la limite est de 10 000, nous verrons une astuce pour contourner ça après).

Le paramètre timeframe est une chaine de caractères correspondant à la résolution des données devant être formatée de la façon suivante :

  • 1m = 1 minute
  • 5m = 5 minutes
  • 1h = 1 heure
  • 4h = 4 heures
  • 1d = 1 jour
  • etc...

On crée donc une méthode format_timeframe pour formater nos paramètres renseignés en entrée dans un format compris par la méthode fetch_ohlcv :

timeframes_mapper = {
    bt.TimeFrame.Minutes: "m",
    bt.TimeFrame.Days: "d",
    bt.TimeFrame.Months: "M",
}

def format_timeframe(self):
     
    if self.p.timeframe == bt.TimeFrame.Minutes:
        if self.p.compression == 60:
            return "1h"
        if self.p.compression == 120:
            return "2h"
        if self.p.compression == 240:
            return "4h"
    return f"{self.p.compression}{timeframes_mapper[self.p.timeframe]}"

timeframes_mapper est un dictionnaire nous permettant de convertir simplement un timeframe passé en paramètre en l'unité correspondante, soit "m", "d", ou "M". Vous ne comprenez peut-être pas tout, car les timeframes sont spécifiques à la librairie backtrader, mais le prochain article traitera de cette librairie donc ne vous inquiétez pas !

Bien, maintenant il ne nous reste plus que le paramètre since à fournir. C'est un paramètre correspondant à la date de début d'extraction de données, fourni sous forme de timestamp. On convertit donc notre date donnée en paramètres en date sous forme de timestamp comme ceci :

timestamp = int(dt.datetime.timestamp(self.p.start_date) * 1000)

(On convertit notre date en timestamp, qu'on multiplie par 1000 pour l'avoir en ms, qu'on transforme en int)

On peut maintenant passer tous nos paramètres à la méthode fetch_ohlcv :

data = exchange.fetch_ohlcv(symbol=self.p.symbol, timeframe=self.format_timeframe(), since=timestamp, limit=10000)

Si vous affichez data, vous voyez qu'on obtient un Dataframe correspondant aux valeurs OHLCV (si vous ne savez pas ce qu'est un Dataframe, l'article sur pandas arrive bientôt).

Mais on ne peut avoir que 10 000 valeurs max, si on en veut plus il faut donc être astucieux :

        while data[-1][0] < dt.datetime.timestamp(self.p.end_date) * 1000:
            data2 = exchange.fetch_ohlcv(symbol=self.p.symbol, timeframe=self.format_timeframe(), since=data[-1][0],
                                         limit=10000)
            for i in range(len(data2)):
                if i != 0:
                    data.append(data2[i])

On regarde si le temps de la 10 000ème valeur de notre Dataframe est inférieur au temps de fin passé en paramètres. S'il l'est, c'est que nous n'avons pas extrait toutes les valeurs, on exécute donc une nouvelle fois la méthode fetch_ohlcv en prenant comme valeur de début le temps de la 10 000ème valeur extraite, ce qui nous permet d'extraire les 10 000 valeurs suivantes. On concatène ensuite les deux Dataframes obtenus. On fait cela jusqu'à avoir toutes les valeurs.

Finalement, on convertit nos données en tuple (nécessaire pour la suite) et on les renvoie :

        for i in range(len(data)):
            data[i] = tuple(data[i])
        return data

On rassemble tout ceci dans une méthode exctract_klines :

    def extract_klines(self):
        """
        Extract klines corresponding to params

        """
        exchange = self.p.exchange
        timestamp = int(dt.datetime.timestamp(self.p.start_date) * 1000)  # Timestamp formatting in ms

        data = exchange.fetch_ohlcv(symbol=self.p.symbol, timeframe=self.format_timeframe(), since=timestamp, limit=10000)
        while data[-1][0] < dt.datetime.timestamp(self.p.end_date) * 1000:  # If more than 10 000 candles
            data2 = exchange.fetch_ohlcv(symbol=self.p.symbol, timeframe=self.format_timeframe(), since=data[-1][0],
                                         limit=10000)
            for i in range(len(data2)):
                if i != 0:
                    data.append(data2[i])
        for i in range(len(data)):
            data[i] = tuple(data[i])
        return data

Désolé si le bloc de code est formaté bizarrement, j'ai copié collé mon code original. Les commentaires sont donc gardés aussi.

Le mot de la fin

Et voilà, c'est tout pour ce premier article à propos de ce projet. J'ai préféré ne pas faire toute la partie extraction de données en un article pour ne pas que ce soit trop lourd.

Puis je me suis également rendu compte en écrivant qu'il y a vraiment beaucoup de choses à savoir et de librairies à connaitre pour pouvoir réaliser un projet comme celui-ci, je préfère donc ne pas aller trop loin et écrire d'autres articles afin d'expliquer plus en détail les librairies vues avant de continuer pour ne pas vous perdre.

J'essaie d'être compréhensible, mais ce n'est pas évident lorsqu'on ne sait pas à qui on s'adresse et qu'on ne connait pas son niveau. Donc, n'hésitez-pas à me faire part de vos retours ou vos questions par message via le site, par mail, ou par commentaire, je me ferai un plaisir de vous répondre !

En espérant que cet article vous aura plus, on se retrouve prochainement pour la suite de ce projet !

Laisser un commentaire

Premium - 15€/mois

L'accès à des articles inédits, à une multitude de ressources, à de nouveaux projets, mais également à des vidéos explicatives, découvrez ici pourquoi passer premium.

Articles liés

Catégories

Ressources

Retrouvez une collection de ressources (des scripts, des fiches résumé, des images...) liées aux articles du blog ou au Python.
Voir

Contact

contact@propython.fr
Se connecter pour envoyer un message directement depuis le site.

Navigation

AccueilSe connecterCréer un compteRessourcesPremium

Catégories

Pages légales

Politique de confidentialitéMentions légalesConditions générales de vente