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é.
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.
Voici les fonctionnalités que je souhaite intégrer à mon bot de trading :
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.
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.
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.
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.
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.
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 :
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 !
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.
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.
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
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.
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 :
Datafeed
. Soit pour le mode live, soit pour le mode backtesting.Datafeed
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 :
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.
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 !