Backtest d’indicateurs techniques sur plusieurs tickers avec Python
Introduction
Ce rapport présente une expérimentation sur les indicateurs techniques avec le projet BatchBacktesting sur GitHub : BatchBacktesting.
Installation des dépendances
Installez les bibliothèques nécessaires :
!pip install numpy httpx richp
Imports
Modules à importer pour le script :
import pandas as pd
import numpy as np
from datetime import datetime
import httpx
import concurrent.futures
import glob
import warnings
from rich.progress import track
warnings.filterwarnings(“ignore”)
Configuration API
Remplacez FMP_API_KEY et BINANCE_API_KEY par vos clés pour accéder aux services concernés.
BASE_URL_FMP = “https://financialmodelingprep.com/api/v3"
BASE_URL_BINANCE = “https://fapi.binance.com/fapi/v1/"
FMP_API_KEY = “YOUR_FMP_API_KEY”
BINANCE_API_KEY = “YOUR_BINANCE_API_KEY”
Fonctions de requêtes API
Ces fonctions appellent différents points de terminaison pour l’historique crypto et actions.
def make_api_request(api_endpoint, params):
with httpx.Client() as client:
response = client.get(api_endpoint, params=params)
if response.status_code == 200:
return response.json()
print(“Error: Failed to retrieve data from API”)
return None
def get_historical_price_full_crypto(symbol):
api_endpoint = f”{BASE_URL_FMP}/historical-price-full/crypto/{symbol}”
params = {“apikey”: FMP_API_KEY}
return make_api_request(api_endpoint, params)
def get_historical_price_full_stock(symbol):
api_endpoint = f"{BASE_URL_FMP}/historical-price-full/{symbol}"
params = {“apikey”: FMP_API_KEY}
return make_api_request(api_endpoint, params)
def get_SP500():
api_endpoint = “https://en.wikipedia.org/wiki/List_of_S%26P_500_companies”
data = pd.read_html(api_endpoint)
return list(data[0][‘Symbol’])
def get_all_crypto():
return [
“BTCUSD”, “ETHUSD”, “LTCUSD”, “BCHUSD”, “XRPUSD”, “EOSUSD”,
“XLMUSD”, “TRXUSD”, “ETCUSD”, “DASHUSD”, “ZECUSD”, “XTZUSD”,
“XMRUSD”, “ADAUSD”, “NEOUSD”, “XEMUSD”, “VETUSD”, “DOGEUSD”,
“OMGUSD”, “ZRXUSD”, “BATUSD”, “USDTUSD”, “LINKUSD”, “BTTUSD”,
“BNBUSD”, “ONTUSD”, “QTUMUSD”, “ALGOUSD”, “ZILUSD”, “ICXUSD”,
“KNCUSD”, “ZENUSD”, “THETAUSD”, “IOSTUSD”, “ATOMUSD”, “MKRUSD”,
“COMPUSD”, “YFIUSD”, “SUSHIUSD”, “SNXUSD”, “UMAUSD”, “BALUSD”,
“AAVEUSD”, “UNIUSD”, “RENBTCUSD”, “RENUSD”, “CRVUSD”, “SXPUSD”,
“KSMUSD”, “OXTUSD”, “DGBUSD”, “LRCUSD”, “WAVESUSD”, “NMRUSD”,
“STORJUSD”, “KAVAUSD”, “RLCUSD”, “BANDUSD”, “SCUSD”, “ENJUSD”
]
def get_financial_statements_lists():
api_endpoint = f"{BASE_URL_FMP}/financial-statement-symbol-lists"
params = {“apikey”: FMP_API_KEY}
return make_api_request(api_endpoint, params)
Stratégie EMA
La moyenne mobile exponentielle (EMA) accorde plus de poids aux points récents ; elle réagit plus vite que la moyenne mobile simple (SMA).
class EMA(Strategy):
n1 = 20
n2 = 80
def init(self):
close = self.data.Close
self.ema20 = self.I(taPanda.ema, close.s, self.n1)
self.ema80 = self.I(taPanda.ema, close.s, self.n2)
def next(self):
price = self.data.Close
if crossover(self.ema20, self.ema80):
self.position.close()
self.buy(sl=0.90 \* price, tp=1.25 \* price)
elif crossover(self.ema80, self.ema20):
self.position.close()
self.sell(sl=1.10 \* price, tp=0.75 \* price)
Dans cette stratégie :
ema20etema80sont calculées pour l’instrument.- Achat quand
ema20croise au-dessus deema80. - Vente quand
ema80croise au-dessus deema20. - Stop loss (
sl) et take profit (tp) pour limiter les pertes et prendre des gains.
Stratégie MACD
Le MACD est un indicateur de momentum qui compare deux moyennes mobiles (souvent EMA 12 et 26) ; la ligne de signal est une EMA 9 du MACD et sert de déclencheur d’achat/vente.
class MACD(Strategy):
short_period = 12
long_period = 26
signal_period = 9
def init(self):
close = self.data.Close
self.macd = self.I(taPanda.macd, close.s, self.short\_period, self.long\_period, self.signal\_period)
def next(self):
macd\_line = self.macd.macd
signal\_line = self.macd.signal
if crossover(macd\_line, signal\_line):
self.position.close()
self.buy()
elif crossover(signal\_line, macd\_line):
self.position.close()
self.sell()
macd_lineetsignal_linedérivent des EMA courtes et longues.- Achat quand
macd_linecroise au-dessus designal_line. - Vente quand
signal_linecroise au-dessus demacd_line.
Exécuter les backtests
Fonctions pour traiter les instruments et lancer les stratégies choisies.
def run_backtests_strategies(instruments, strategies):
strategies = [x for x in STRATEGIES if x.__name__ in strategies]
outputs = []
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = []
for strategy in strategies:
future = executor.submit(run_backtests, instruments, strategy, 4)
futures.append(future)
for future in concurrent.futures.as_completed(futures):
outputs.extend(future.result())
return outputs
def check_crypto(instrument):
return instrument in get_all_crypto()
def check_stock(instrument):
return instrument not in get_financial_statements_lists()
def process_instrument(instrument, strategy):
try:
if check_crypto(instrument):
data = get_historical_price_full_crypto(instrument)
else:
data = get_historical_price_full_stock(instrument)
if data is None or “historical” not in data:
print(f"Error processing {instrument}: No data")
return None
data = clean_data(data)
bt = Backtest(data, strategy=strategy, cash=100000, commission=0.002, exclusive_orders=True)
output = bt.run()
output = process_output(output, instrument, strategy)
return output, bt
except Exception as e:
print(f"Error processing {instrument}: {str(e)}")
return None
def clean_data(data):
data = data[“historical”]
data = pd.DataFrame(data)
data.columns = [x.title() for x in data.columns]
data = data.drop([“Adjclose”, “Unadjustedvolume”, “Change”, “Changepercent”, “Vwap”, “Label”, “Changeovertime”], axis=1)
data[“Date”] = pd.to_datetime(data[“Date”])
data.set_index(“Date”, inplace=True)
data = data.iloc[::-1]
return data
def process_output(output, instrument, strategy, in_row=True):
if in_row:
output = pd.DataFrame(output).T
output[“Instrument”] = instrument
output[“Strategy”] = strategy.__name__
output.pop("_strategy")
return output
def save_output(output, output_dir, instrument, start, end):
print(f"Saving output for {instrument}")
fileNameOutput = f"{output_dir}/{instrument}-{start}-{end}.csv"
output.to_csv(fileNameOutput)
def plot_results(bt, output_dir, instrument, start, end):
print(f"Saving chart for {instrument}")
fileNameChart = f"{output_dir}/{instrument}-{start}-{end}.html"
bt.plot(filename=fileNameChart, open_browser=False)
def run_backtests(instruments, strategy, num_threads=4, generate_plots=False):
outputs = []
output_dir = f"output/raw/{strategy.__name__}"
output_dir_charts = f"output/charts/{strategy.__name__}"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
if not os.path.exists(output_dir_charts):
os.makedirs(output_dir_charts)
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
future_to_instrument = {executor.submit(process_instrument, instrument, strategy): instrument for instrument in instruments}
for future in concurrent.futures.as_completed(future_to_instrument):
instrument = future_to_instrument[future]
output = future.result()
if output is not None:
outputs.append(output[0])
save_output(output[0], output_dir, instrument, output[0][“Start”].to_string().strip().split()[1], output[0][“End”].to_string().strip().split()[1])
if generate_plots:
plot_results(output[1], output_dir_charts, instrument, output[0][“Start”].to_string().strip().split()[1], output[0][“End”].to_string().strip().split()[1])
data_frame = pd.concat(outputs)
start = data_frame[“Start”].to_string().strip().split()[1]
end = data_frame[“End”].to_string().strip().split()[1]
fileNameOutput = f"output/{strategy.__name__}-{start}-{end}.csv"
data_frame.to_csv(fileNameOutput)
return data_frame
Lancer les scripts
tickers = get_SP500()
run_backtests(tickers, strategy=EMA, num_threads=12, generate_plots=True)
run_backtests(tickers, strategy=MACD, num_threads=12, generate_plots=True)
ticker = get_all_crypto()
run_backtests(ticker, strategy=EMA, num_threads=12, generate_plots=True)
run_backtests(ticker, strategy=MACD, num_threads=12, generate_plots=True)
Le dossier output du dépôt BatchBacktesting ne contient en général pas de résultats précalculés — les auteurs évitent d’y versionner des données spécifiques à chaque utilisateur.
Pour obtenir des chiffres, exécutez le script localement avec vos paramètres et stratégies ; les sorties iront dans le répertoire output du projet.
Exemple de graphique de référence : EMA — AAPL.
Analyse des résultats
Exemple de classement EMA (rendements les plus hauts et plus bas) :
- Cinq instruments avec les rendements les plus élevés :
- BTCBUSD: 293.78%
- ALB: 205.97%
- OMGUSD: 199.62%
- BBWI: 196.82%
- GRMN: 193.47%
- Cinq instruments avec les rendements les plus faibles :
- BTTBUSD: -99.93%
- UAL: -82.63%
- NCLH: -81.51%
- LNC: -78.02%
- CHRW: -76.38%

Conclusion
BatchBacktesting offre une approche souple pour tester des indicateurs techniques sur actions et crypto. Les fonctions fournies s’intègrent aux APIs financières et simplifient la manipulation des données. Les résultats expérimentaux peuvent nourrir l’affinage de stratégies algorithmiques — en gardant à l’esprit sur-ajustement et réalité des frais.
Publié à l’origine sur Medium.