Analyser le backtest d'un portefeuille de trading en Python grâce à Pandas
Nous avons franchi une étape cruciale en maîtrisant le backtesting de nos stratégies de trading.
Cependant, la création d'une stratégie n'est que la première étape. L'analyse approfondie des résultats est là où se joue la distinction entre une bonne et une excellente stratégie.
Aujourd'hui, nous abordons cette phase critique avec une précision et une rigueur scientifiques. Nous allons explorer les outils analytiques et les techniques d'évaluation qui transforment des données brutes en informations précises, permettant une prise de décision réfléchie et éclairée.
Chaque détail, chaque donnée, chaque signal sera scruté afin d'affiner et de perfectionner nos interventions sur le marché. Cette étape est décisive, elle marque le passage de la théorie à une pratique affinée, où chaque décision est soutenue par une analyse méthodique et précise. Bienvenue dans cette leçon essentielle, où la qualité de notre analyse définira la robustesse et l'efficacité de nos stratégies de trading.
Préparer l’analyse de notre stratégie de trading
Pour commencer, reprenons le code de notre séance précédente.
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,
'open_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['open_size']
position = None
print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")
print(f"Final balance: {balance}$")
Prendre en compte les frais de trading
Il est essentiel de reconnaître qu'aucune stratégie de trading n'est complète sans prendre en compte les frais associés aux transactions. Nous avons élaboré un modèle solide avec notre classe BacktestCrossMA
, mais aujourd'hui, nous allons l'affiner en intégrant les frais de transaction. Ce détail apparemment mineur peut avoir un impact considérable, et sa prise en compte rendra nos résultats de backtesting plus précis et nos stratégies plus robustes dans un environnement de trading réel.
Pour ce faire, nous allons commencer par initialiser une variable fees
à 0.001
pour signifier qu’à chaque transaction, nous allons payer 0.1% de frais. Cela est une estimation qui peut grandement varier en fonction des brokers avec lesquels vous interagirez.
def run_backtest(self):
balance = 1000
position = None
asset = "BTC"
fees = 0.001
for index, row in self.df.iterrows():
if position is None and row['buy_signal']:
open_price = row['close']
open_size = balance
fee = open_size * fees
open_size = open_size - fee
balance = balance - fee
position = {
'open_price': open_price,
'open_size': open_size,
}
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']
close_size = (position["open_size"] + position["open_size"] * trade_result)
fee = close_size * fees
close_size = close_size - fee
balance = balance + close_size - position["open_size"]
position = None
print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")
Pour prendre en compte les frais, au moment de l’achat et de la vente, nous avons défini une variable fee
représentant les frais associé à la transaction. Les frais payés sont égaux au montant de la transaction fois notre variable fees
. Nous avons également rajouté les variables open_size
et close_size
afin de pouvoir y appliquer nos frais puis, qu’ils se répercutent sur notre balance finale.
Si nous exécutions le code maintenant, nous remarquons que notre balance finale est maintenant de 10 458
dollars au lieu des 12 756
dollars initiaux, ce qui reste très bon, mais qui n’est pas à négliger.
Suivre notre portefeuille chaque jour
Afin de faire un suivi complet de notre stratégie, nous allons chaque jour enregistrer notre balance dans une liste. Au début de notre fonction **run_backtest()**
, nous allons donc initialiser notre liste days
et par la même occasion, initialiser une liste trades
qui nous permettra d’enregistrer ensuite chacun de nos trades.
self.trades = []
self.days = []
Nous utilisons self afin que ces listes soient accessibles partout dans notre classe.
Pour rappel, notre DataFrame df représente le cours OHLCV du Bitcoin heure par heure. Cependant, nous souhaitons récupérer l’état de notre portefeuille une fois par jour.
Pour cela, nous pouvons à chaque heure regarder le jour associé et le comparer au jour précédant. Si le jour actuel est différent du jour précédent, alors c’est un nouveau jour et nous pouvons donc enregistrer une donnée dans notre liste self.days
. Voici le code pour faire ceci :
def run_backtest(self):
balance = 1000
position = None
fees = 0.0007
asset = "BTC"
self.trades = []
self.days = []
previous_day = -1
for index, row in self.df.iterrows():
current_day = index.day
if previous_day != current_day:
temp_balance = balance
if position:
close_price = row["close"]
trade_result = (close_price - position["open_price"]) / position[
"open_price"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
temp_balance = temp_balance + close_size - position["open_size"]
self.days.append({
"day":index.date(),
"balance":temp_balance,
"price":row['close']
})
previous_day = current_day
Nous initialisons la variable previous_day
à -1
, lors de notre itération, nous vérifions le jour actuel grâce à index.day
(pour rappel, nous avions mis les dates en index de notre DataFrame).
Si nous avons une position en cours, nous simulons le fait que nous la fermions pour avoir son impact sur notre balance. Enfin, nous stockons la date, notre balance temporaire et le prix de l’actif dans un dictionnaire que nous ajoutons à notre liste self.days
.
Stocker chacun des trades de la stratégie
Afin de pouvoir réaliser une analyse complète par la suite, nous devons stocker dans notre liste self.trades
l’ensemble de nos trades avec les informations nécessaires. Pour commencer, nous allons ajouter lors de l’entrée en position certaines informations :
if position is None and row["buy_signal"]:
open_price = row["close"]
open_size = balance
fee = open_size * fees
open_size = open_size - fee
balance = balance - fee
position = {
"open_price": open_price,
"open_size": balance,
"open_date": index,
"open_fee": fee,
"open_reason": "Market Buy",
"open_balance": balance,
}
Par rapport au code initial, nous ajoutons la date d’ouverture de la position, les frais associés, la raison de l’ouverture et la balance à l’ouverture de la position.
Maintenant, lors de la fermeture de la position, comme lorsque nous ajoutions les informations liées à la journée en cours, créons un dictionnaire avec les différentes informations liées à notre trade:
elif position and row["sell_signal"]:
close_price = row["close"]
trade_result = (close_price - position["open_price"]) / position[
"open_price"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
balance = balance + close_size - position["open_size"]
self.trades.append(
{
"open_date": position["open_date"],
"close_date": index,
"open_price": position["open_price"],
"close_price": close_price,
"open_size": position["open_size"],
"close_size": close_size,
"open_fee": position["open_fee"],
"close_fee": fee,
"open_reason": position["open_reason"],
"close_reason": "Market Sell",
"open_balance": position["open_balance"],
"close_balance": balance,
}
)
position = None
Grâce à ce code, lorsque nous clôturons une position, nous ajoutons à notre liste self.trades
:
open_date
close_date
open_price
close_price
open_size
close_size
open_fee
close_fee
open_reason
close_reason
open_balance
close_balance
Vous retrouverez ci-dessous, le code de la fonction run_backtest()
avec toutes les nouvelles modifications :
def run_backtest(self):
balance = 1000
position = None
fees = 0.0007
self.trades = []
self.days = []
previous_day = -1
for index, row in self.df.iterrows():
current_day = index.day
if previous_day != current_day:
temp_balance = balance
if position:
close_price = row["close"]
trade_result = (close_price - position["open_price"]) / position[
"open_price"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
temp_balance = temp_balance + close_size - position["open_size"]
self.days.append({
"day":index.date(),
"balance":temp_balance,
"price":row['close']
})
previous_day = current_day
if position is None and row["buy_signal"]:
open_price = row["close"]
open_size = balance
fee = open_size * fees
open_size = open_size - fee
balance = balance - fee
position = {
"open_price": open_price,
"open_size": balance,
"open_date": index,
"open_fee": fee,
"open_reason": "Market Buy",
"open_balance": 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"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
balance = balance + close_size - position["open_size"]
self.trades.append(
{
"open_date": position["open_date"],
"close_date": index,
"open_price": position["open_price"],
"close_price": close_price,
"open_size": position["open_size"],
"close_size": close_size,
"open_fee": position["open_fee"],
"close_fee": fee,
"open_reason": position["open_reason"],
"close_reason": "Market Sell",
"open_balance": position["open_balance"],
"close_balance": balance,
}
)
position = None
# print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")
Analyser les résultats de notre stratégie grâce à Pandas
Avec les données de nos journées et transactions de trading soigneusement enregistrées dans self.days
et self.trades
, nous sommes maintenant prêts à avancer vers une étape cruciale de notre processus d'évaluation: l'analyse des métriques de performance.
Cette phase est fondamentale pour décrypter et évaluer la validité et l'efficacité de notre stratégie de trading dans un contexte opérationnel.
Dans le monde complexe et dynamique du trading, une stratégie ne peut être considérée comme viable que si elle a été rigoureusement testée et validée à l'aide de critères quantitatifs précis.
Ces métriques, allant des rendements ajustés au risque jusqu'aux drawdowns maximaux, fournissent des insights précieux sur la robustesse, la rentabilité et la résilience de notre approche.
Chaque transaction et variation quotidienne sera scrutée pour assurer que notre stratégie non seulement génère des rendements positifs, mais le fait aussi de manière consistante et fiable, tout en minimisant le risque.
Cela garantira que la stratégie est bien équipée pour naviguer dans les diverses conditions de marché, en assurant une performance optimale.
Pour ce faire, nous allons créer une nouvelle fonction à l’intérieur de notre classe intitulée backtest_analysis()
.
Initialisation et Vérification des Données
df_trades = pd.DataFrame(self.trades)
df_days = pd.DataFrame(self.days)
if df_trades.empty:
raise Exception("No trades found")
if df_days.empty:
raise Exception("No days found")
Dans cette première section de la fonction backtest_analysis
, nous initialisons deux DataFrames, df_trades
et df_days
, à partir des données de trades et de jours stockées lors du backtest. Nous effectuons également une vérification essentielle pour nous assurer que ces DataFrames ne sont pas vides. Si l'une d'elles est vide, une exception est levée, indiquant l'absence de trades ou de jours, ce qui est crucial pour éviter des erreurs d'exécution ultérieures dans l'analyse.
Calcul des Retours et Résultats des Trades
df_days['evolution'] = df_days['balance'].diff()
df_days['daily_return'] = df_days['evolution']/df_days['balance'].shift(1)
df_trades["trade_result"] = df_trades["close_size"] - df_trades["open_size"]
df_trades["trade_result_pct"] = (df_trades["trade_result"] / df_trades["open_size"])
df_trades["trades_duration"] = df_trades["close_date"] - df_trades["open_date"]
Nous passons ensuite au calcul des retours quotidiens et des résultats des trades. Nous déterminons l'évolution du solde jour après jour et calculons le retour quotidien.
Pour les trades, nous calculons le résultat de chaque trade ainsi que le pourcentage de ce résultat par rapport à la taille d'ouverture du trade. Nous calculons par ailleurs la durée de chaque trade pour avoir une vue détaillée de la rapidité avec laquelle les trades ont été conclus.
Analyse des Drawdowns et Performance
df_days["balance_ath"] = df_days["balance"].cummax()
df_days["drawdown"] = df_days["balance_ath"] - df_days["balance"]
df_days["drawdown_pct"] = df_days["drawdown"] / df_days["balance_ath"]
L'analyse des drawdowns est un aspect essentiel pour évaluer le risque associé à notre stratégie de trading. Nous calculons la balance maximale atteinte à ce jour (All Time High - ATH) et utilisons cette information pour déterminer le drawdown en valeur et en pourcentage. Ces métriques fournissent un aperçu précieux de la perte maximale subie pendant la période de backtest.
Détails des Trades
total_trades = len(df_trades)
total_days = len(df_days)
good_trades = df_trades.loc[df_trades["trade_result"] > 0]
total_good_trades = len(good_trades)
avg_profit_good_trades = good_trades["trade_result_pct"].mean()
mean_good_trades_duration = good_trades["trades_duration"].mean()
global_win_rate = total_good_trades / total_trades
bad_trades = df_trades.loc[df_trades["trade_result"] < 0]
total_bad_trades = len(bad_trades)
avg_profit_bad_trades = bad_trades["trade_result_pct"].mean()
mean_bad_trades_duration = bad_trades["trades_duration"].mean()
max_days_drawdown = df_days["drawdown_pct"].max()
initial_balance = df_days.iloc[0]["balance"]
final_balance = df_days.iloc[-1]["balance"]
balance_evolution = (final_balance - initial_balance) / initial_balance
mean_trades_duration = df_trades["trades_duration"].mean()
avg_profit = df_trades["trade_result_pct"].mean()
mean_trades_duration = df_trades['trades_duration'].mean()
mean_trades_per_days = total_trades/total_days
best_trade = df_trades.loc[df_trades["trade_result_pct"].idxmax()]
worst_trade = df_trades.loc[df_trades["trade_result_pct"].idxmin()]
Dans cette section, une analyse approfondie des trades est effectuée. Nous calculons le nombre total de trades gagnants et perdants, le profit moyen pour chaque catégorie et la durée moyenne des trades. Ces informations sont cruciales pour comprendre la qualité de la stratégie et identifier les domaines potentiels d'amélioration.
Synthèse et Affichage
print(f"Period: [{df_days.iloc[0]['day']}] -> [{df_days.iloc[-1]['day']}]")
print(f"Initial balance: {round(initial_balance,2)} $")
print("\n--- General Information ---")
print(f"Final balance: {round(final_balance,2)} $")
print(f"Performance: {round(balance_evolution*100,2)} %")
print(f"Worst Drawdown: -{round(max_days_drawdown*100, 2)}%")
print(f"Total trades on the period: {total_trades}")
print(f"Average Profit: {round(avg_profit*100, 2)} %")
print(f"Global Win rate: {round(global_win_rate*100, 2)} %")
print("\n--- Trades Information ---")
print(f"Mean Trades per day: {round(mean_trades_per_days, 2)}")
print(f"Mean Trades Duration: {mean_trades_duration}")
print(f"Best trades: +{round(best_trade['trade_result_pct']*100, 2)} % the {best_trade['open_date']} -> {best_trade['close_date']}")
print(f"Worst trades: {round(worst_trade['trade_result_pct']*100, 2)} % the {worst_trade['open_date']} -> {worst_trade['close_date']}")
print(f"Total Good trades on the period: {total_good_trades}")
print(f"Total Bad trades on the period: {total_bad_trades}")
print(f"Average Good Trades result: {round(avg_profit_good_trades*100, 2)} %")
print(f"Average Bad Trades result: {round(avg_profit_bad_trades*100, 2)} %")
print(f"Mean Good Trades Duration: {mean_good_trades_duration}")
print(f"Mean Bad Trades Duration: {mean_bad_trades_duration}")
print("\n--- Trades reasons ---")
print(df_trades["open_reason"].value_counts().to_string())
print(df_trades["close_reason"].value_counts().to_string())
La fonction affiche enfin des informations détaillées sur la performance globale de la stratégie, y compris la balance finale, la performance globale, le pire drawdown et d'autres métriques clés. Cette sortie structurée offre un résumé complet des résultats du backtest, permettant une évaluation rapide et approfondie de la stratégie de trading.
Petit plus : Le Sharpe Ratio
Le ratio de Sharpe mesure la performance ajustée au risque d'un investissement ou d'une stratégie de trading, en comparant le rendement par rapport à sa volatilité. Un ratio de Sharpe plus élevé indique une performance ajustée au risque plus favorable, offrant des rendements plus élevés pour un niveau de risque donné. Ce ratio est un des plus pertinents pour comparer deux stratégies entre elles.
Voici comment le calculer grâce aux informations journalières de notre stratégie
sharpe_ratio = (365**(0.5) * df_days['daily_return'].mean())/df_days['daily_return'].std()
print(f"Sharpe Ratio: {round(sharpe_ratio,2)}")
Nous annualisons notre ratio de Sharpe en multipliant notre rendement journalier par la racine carré de 365 et nous divisons le tout par l’écart type.
Bonus : Ajouter un Stop Loss au backtest
Un stop loss est un ordre de vente automatique utilisé par les traders pour limiter leurs pertes en cas de mouvement défavorable du marché. Il est déclenché lorsque le prix d'un actif atteint un niveau prédéterminé, forçant ainsi la vente de l'actif pour éviter des pertes supplémentaires. En d'autres termes, c'est un mécanisme de protection qui permet de gérer et de minimiser les risques associés à la volatilité du marché.
Pour ajouter un stop loss à notre stratégie, il faudra lors de l’achat définir son seuil. Pour l’exemple mettons notre stop loss à -10 pourcent.
stop_loss = open_price - (open_price * 0.1)
position = {
"open_price": open_price,
"open_size": balance,
"open_date": index,
"open_fee": fee,
"open_reason": "Market Buy",
"open_balance": balance,
"stop_loss": stop_loss,
}
Dans ce cas, nous ajoutons à notre dictionnaire position, le prix auquel nous souhaitons couper la position si l’on passe en dessous.
Ensuite, nous allons ajouter une nouvelle condition lors de notre itération:
elif position and row["low"] < position["stop_loss"]:
close_price = position["stop_loss"]
trade_result = (close_price - position["open_price"]) / position"open_price"]
close_size = (position["open_size"] + position["open_size"] * trade_result)
fee = close_size * fees
close_size = close_size - fee
balance = balance + close_size - position["open_size"]
self.trades.append(
{
"open_date": position["open_date"],
"close_date": index,
"open_price": position["open_price"],
"close_price": close_price,
"open_size": position["open_size"],
"close_size": close_size,
"open_fee": position["open_fee"],
"close_fee": fee,
"open_reason": position["open_reason"],
"close_reason": "Stop Loss",
"open_balance": position["open_balance"],
"close_balance": balance,
}
)
position = None
Nous entrons dans cette condition si le point le plus bas d’une bougie (le low) passe en dessous de la valeur de notre stop loss. Dans ce cas nous vendons nos Bitcoin au prix du stop loss sur le même principe que la vente “classique”.
Les graphiques pour évaluer notre stratégie
Il existe des centaines de graphique à raliser à partir des informations récoltées sur la stratégie. Nous allons ici nous concentrer sur les informations les plus importantes qui sont le P&L (Pofit and Lose) qui représente simplement l’évolution de la balance de notre stratégie et le Drawdown (Perte par rapport au dernier plus haut).
def plot_equity_vs_asset(self):
df_days = self.df_days.copy()
df_days = df_days.set_index('day')
fig, ax_left = plt.subplots(figsize=(15, 20), nrows=4, ncols=1)
ax_left[0].title.set_text("Profit and Lose curve")
ax_left[0].plot(df_days['balance'], color='royalblue', lw=1)
ax_left[0].fill_between(df_days['balance'].index, df_days['balance'], alpha=0.2, color='royalblue')
ax_left[0].axhline(y=df_days.iloc[0]['balance'], color='black', alpha=0.3)
ax_left[0].legend(['Wallet evolution (equity)'], loc ="upper left")
ax_left[1].title.set_text("Asset evolution")
ax_left[1].plot(df_days['price'], color='sandybrown', lw=1)
ax_left[1].fill_between(df_days['price'].index, df_days['price'], alpha=0.2, color='sandybrown')
ax_left[1].axhline(y=df_days.iloc[0]['price'], color='black', alpha=0.3)
ax_left[1].legend(['Asset evolution'], loc ="upper left")
ax_left[2].title.set_text("Drawdown curve")
ax_left[2].plot(-df_days['drawdown_pct']*100, color='indianred', lw=1)
ax_left[2].fill_between(df_days['drawdown_pct'].index, -df_days['drawdown_pct']*100, alpha=0.2, color='indianred')
ax_left[2].axhline(y=0, color='black', alpha=0.3)
ax_left[2].legend(['Drawdown in %'], loc ="lower left")
ax_right = ax_left[3].twinx()
ax_left[3].title.set_text("P&L VS Asset (not on the same scale)")
ax_left[3].plot(df_days['balance'], color='royalblue', lw=1)
ax_right.plot(df_days['price'], color='sandybrown', lw=1)
ax_left[3].legend(['Wallet evolution (equity)'], loc ="lower right")
ax_right.legend(['Asset evolution'], loc ="upper left")
plt.show()
Nous ne retrerons pas en détail sur ce code dans cette séance mais voici les quelque clés pour le comprendre :
- Nous commençons par définir notre graphique comme étant découper en 4 sous graphiques grâce à la ligne
fig, ax_left = plt.subplots(figsize=(15, 20), nrows=4, ncols=1)
- Nous affichone ensuite le premier graphique avec
ax_left[0]
puis le deuxième avecax_left[1]
etc… - Pour le dernier graphique nous créons un deuxième axe à droite qui est une copie de l’axe gauche du dernier sous graphique grâce à
ax_right = ax_left[3].twinx()
, cet axe permettra d’afficher sur le même graphique, le cours de notre actif (le Bitcoin ici) et l’évolution de notre balance. Cela permet notamment de voir si les 2 courbes ont des variations en même temps et donc si elles sont corrélées.
Notre stratégie avec Stop Loss qui a commencé à 1000 dollars fin 2017 finit à près de 8000 dollars en 2023. On remarque que la stratégie au plus haut est montée à une balance de 12 000 dollars début 2021. Depuis nous ne sommes jamais remonté à ce niveau et nous avons eu un drawdown maximum de plus de -50%.
On trouve également que l’on a une très forte corrélation entre la performance de notre stratégie et la performance du Bitcoin ce qui est au final plutôt logique car en ne faisant que acheter (parier à la hausse) nous ne pouvons gagner de l’argent que si le Bitcoin monte.
Nous avons abordé et approfondi des éléments clés pour la conception, l'exécution et l'analyse de stratégies de trading efficaces. Avec la capacité de créer des stratégies sophistiquées, l'intégration de stop loss pour gérer et minimiser les risques, et des outils d'analyse approfondie, nous sommes désormais équipés pour faire des évaluations précises.
Que ce soit par le biais de données textuelles ou de visualisations graphiques, nous pouvons disséquer chaque élément de notre stratégie, nous permettant d'affiner et d'optimiser chaque aspect pour maximiser la performance. Ainsi, nous sommes non seulement préparés pour comprendre les mécanismes sous-jacents de nos opérations, mais aussi pour choisir et appliquer la meilleure stratégie de trading possible dans des conditions de marché variées.
Code complet de la classe et comment l’utiliser :
import pandas as pd
import matplotlib.pyplot as plt
class BacktestCrossMA:
def __init__(self) -> None:
self.df = pd.DataFrame()
self.trades = []
self.days = []
self.df_days = pd.DataFrame()
self.df_trades = 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["ma100"] = self.df["close"].rolling(100).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["ma100"] > self.df["ma200"]), "buy_signal"] = True
self.df.loc[(self.df["ma100"] < self.df["ma200"]), "sell_signal"] = True
def run_backtest(self):
balance = 1000
position = None
fees = 0.0007
self.trades = []
self.days = []
previous_day = -1
for index, row in self.df.iterrows():
current_day = index.day
if previous_day != current_day:
temp_balance = balance
if position:
close_price = row["close"]
trade_result = (close_price - position["open_price"]) / position[
"open_price"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
temp_balance = temp_balance + close_size - position["open_size"]
self.days.append({
"day":index.date(),
"balance":temp_balance,
"price":row['close']
})
previous_day = current_day
if position is None and row["buy_signal"]:
open_price = row["close"]
open_size = balance
fee = open_size * fees
open_size = open_size - fee
balance = balance - fee
stop_loss = open_price - (open_price * 0.1)
position = {
"open_price": open_price,
"open_size": balance,
"open_date": index,
"open_fee": fee,
"open_reason": "Market Buy",
"open_balance": balance,
"stop_loss": stop_loss,
}
# print(f"{index} - Buy for {balance}$ of {asset} at {open_price}$")
elif position and row["low"] < position["stop_loss"]:
close_price = position["stop_loss"]
trade_result = (close_price - position["open_price"]) / position[
"open_price"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
balance = balance + close_size - position["open_size"]
self.trades.append(
{
"open_date": position["open_date"],
"close_date": index,
"open_price": position["open_price"],
"close_price": close_price,
"open_size": position["open_size"],
"close_size": close_size,
"open_fee": position["open_fee"],
"close_fee": fee,
"open_reason": position["open_reason"],
"close_reason": "Stop Loss",
"open_balance": position["open_balance"],
"close_balance": balance,
}
)
position = None
# print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")
elif position and row["sell_signal"]:
close_price = row["close"]
trade_result = (close_price - position["open_price"]) / position[
"open_price"
]
close_size = (
position["open_size"] + position["open_size"] * trade_result
)
fee = close_size * fees
close_size = close_size - fee
balance = balance + close_size - position["open_size"]
self.trades.append(
{
"open_date": position["open_date"],
"close_date": index,
"open_price": position["open_price"],
"close_price": close_price,
"open_size": position["open_size"],
"close_size": close_size,
"open_fee": position["open_fee"],
"close_fee": fee,
"open_reason": position["open_reason"],
"close_reason": "Market Sell",
"open_balance": position["open_balance"],
"close_balance": balance,
}
)
position = None
# print(f"{index} - Sell for {balance}$ of {asset} at {close_price}$")
def backtest_analysis(self):
df_trades = pd.DataFrame(self.trades)
df_days = pd.DataFrame(self.days)
if df_trades.empty:
raise Exception("No trades found")
if df_days.empty:
raise Exception("No days found")
df_days['evolution'] = df_days['balance'].diff()
df_days['daily_return'] = df_days['evolution']/df_days['balance'].shift(1)
df_trades["trade_result"] = df_trades["close_size"] - df_trades["open_size"]
df_trades["trade_result_pct"] = (
df_trades["trade_result"] / df_trades["open_size"]
)
df_trades["trades_duration"] = df_trades["close_date"] - df_trades["open_date"]
df_days["balance_ath"] = df_days["balance"].cummax()
df_days["drawdown"] = df_days["balance_ath"] - df_days["balance"]
df_days["drawdown_pct"] = df_days["drawdown"] / df_days["balance_ath"]
total_trades = len(df_trades)
total_days = len(df_days)
good_trades = df_trades.loc[df_trades["trade_result"] > 0]
total_good_trades = len(good_trades)
avg_profit_good_trades = good_trades["trade_result_pct"].mean()
mean_good_trades_duration = good_trades["trades_duration"].mean()
global_win_rate = total_good_trades / total_trades
bad_trades = df_trades.loc[df_trades["trade_result"] < 0]
total_bad_trades = len(bad_trades)
avg_profit_bad_trades = bad_trades["trade_result_pct"].mean()
mean_bad_trades_duration = bad_trades["trades_duration"].mean()
max_days_drawdown = df_days["drawdown_pct"].max()
initial_balance = df_days.iloc[0]["balance"]
final_balance = df_days.iloc[-1]["balance"]
balance_evolution = (final_balance - initial_balance) / initial_balance
mean_trades_duration = df_trades["trades_duration"].mean()
avg_profit = df_trades["trade_result_pct"].mean()
mean_trades_duration = df_trades['trades_duration'].mean()
mean_trades_per_days = total_trades/total_days
best_trade = df_trades.loc[df_trades["trade_result_pct"].idxmax()]
worst_trade = df_trades.loc[df_trades["trade_result_pct"].idxmin()]
sharpe_ratio = (365**(0.5) * df_days['daily_return'].mean())/df_days['daily_return'].std()
print(f"Period: [{df_days.iloc[0]['day']}] -> [{df_days.iloc[-1]['day']}]")
print(f"Initial balance: {round(initial_balance,2)} $")
print("\n--- General Information ---")
print(f"Final balance: {round(final_balance,2)} $")
print(f"Sharpe Ratio: {round(sharpe_ratio,2)}")
print(f"Performance: {round(balance_evolution*100,2)} %")
print(f"Worst Drawdown: -{round(max_days_drawdown*100, 2)}%")
print(f"Total trades on the period: {total_trades}")
print(f"Average Profit: {round(avg_profit*100, 2)} %")
print(f"Global Win rate: {round(global_win_rate*100, 2)} %")
print("\n--- Trades Information ---")
print(f"Mean Trades per day: {round(mean_trades_per_days, 2)}")
print(f"Mean Trades Duration: {mean_trades_duration}")
print(f"Best trades: +{round(best_trade['trade_result_pct']*100, 2)} % the {best_trade['open_date']} -> {best_trade['close_date']}")
print(f"Worst trades: {round(worst_trade['trade_result_pct']*100, 2)} % the {worst_trade['open_date']} -> {worst_trade['close_date']}")
print(f"Total Good trades on the period: {total_good_trades}")
print(f"Total Bad trades on the period: {total_bad_trades}")
print(f"Average Good Trades result: {round(avg_profit_good_trades*100, 2)} %")
print(f"Average Bad Trades result: {round(avg_profit_bad_trades*100, 2)} %")
print(f"Mean Good Trades Duration: {mean_good_trades_duration}")
print(f"Mean Bad Trades Duration: {mean_bad_trades_duration}")
print("\n--- Trades reasons ---")
print(df_trades["open_reason"].value_counts().to_string())
print(df_trades["close_reason"].value_counts().to_string())
self.df_days = df_days
self.df_trades = df_trades
def plot_equity_vs_asset(self):
df_days = self.df_days.copy()
df_days = df_days.set_index('day')
fig, ax_left = plt.subplots(figsize=(15, 20), nrows=4, ncols=1)
ax_left[0].title.set_text("Profit and Lose curve")
ax_left[0].plot(df_days['balance'], color='royalblue', lw=1)
ax_left[0].fill_between(df_days['balance'].index, df_days['balance'], alpha=0.2, color='royalblue')
ax_left[0].axhline(y=df_days.iloc[0]['balance'], color='black', alpha=0.3)
ax_left[0].legend(['Balance evolution'], loc ="upper left")
ax_left[1].title.set_text("Asset evolution")
ax_left[1].plot(df_days['price'], color='sandybrown', lw=1)
ax_left[1].fill_between(df_days['price'].index, df_days['price'], alpha=0.2, color='sandybrown')
ax_left[1].axhline(y=df_days.iloc[0]['price'], color='black', alpha=0.3)
ax_left[1].legend(['Asset evolution'], loc ="upper left")
ax_left[2].title.set_text("Drawdown curve")
ax_left[2].plot(-df_days['drawdown_pct']*100, color='indianred', lw=1)
ax_left[2].fill_between(df_days['drawdown_pct'].index, -df_days['drawdown_pct']*100, alpha=0.2, color='indianred')
ax_left[2].axhline(y=0, color='black', alpha=0.3)
ax_left[2].legend(['Drawdown in %'], loc ="lower left")
ax_right = ax_left[3].twinx()
ax_left[3].title.set_text("P&L VS Asset (not on the same scale)")
ax_left[3].plot(df_days['balance'], color='royalblue', lw=1)
ax_right.plot(df_days['price'], color='sandybrown', lw=1)
ax_left[3].legend(['Wallet evolution (equity)'], loc ="lower right")
ax_right.legend(['Asset evolution'], loc ="upper left")
plt.show()
bt = BacktestCrossMA()
bt.load_data('BTC-USDT.csv')
bt.populate_indicators()
bt.populate_signals()
bt.run_backtest()
bt.backtest_analysis()
bt.plot_equity_vs_asset()
Mise en pratique
Ceci marque le dernier épisode de notre formation "Python pour la Finance". Par conséquent, nous ne fournirons pas de mise en pratique structurée comme auparavant. C'est désormais à vous d'appliquer vos compétences acquises pour répondre à vos besoins spécifiques et de pousser vos limites pour explorer de nouveaux horizons.
Cependant, pour continuer sur le même thème, nous vous suggérons d'expérimenter avec de nouvelles stratégies de trading, d'intégrer des mécanismes tels que les Take Profit - qui fonctionnent, en essence, comme l'inverse des Stop Loss - pour sécuriser vos gains lorsque les marchés évoluent en votre faveur.
En outre, enrichissez votre analyse en intégrant des mesures supplémentaires telles que le nombre de jours en gain, en perte, la plus longue série de gains ou de pertes, entre autres. Explorez l’incorporation de nouveaux graphiques et visualisations pour offrir des perspectives plus nuancées et détaillées de vos données et résultats de trading. Une suggestion de correction se trouve ici : https://github.com/RobotTraders/Python_For_Finance/blob/main/exercise_correction_chapter_10.ipynb.
Rappelez-vous, les possibilités sont infinies; chaque ajout et ajustement est une étape vers l'affinement de votre approche pour naviguer avec précision dans l'environnement dynamique et complexe de la finance. Bonne exploration !
Analyser le backtest d'un portefeuille de trading en Python grâce à Pandas
Créez et automatisez vos stratégies de trading grâce à ChatGPT o1, Trading View et les Signal Bots
Le Guide sur le Copy Trading : Réalités, Pièges et Outils d’Analyse pour Trouver des Traders Fiables
Stratégie de Trading Automatisée TRIX (Suivi de Tendance)
Optimiser Votre Portefeuille Financier en 2024 : ETF, Actions, Cryptomonnaies, Obligations et Métaux Précieux
Optimisez vos Stratégies de Trading avec les Bougies Basées sur Volume