Système d'arbitrage à haute fréquence en Python utilisant MetaTrader 5
Introduction
Marché des changes. Stratégies algorithmiques. Python et MetaTrader 5. Cela s'est concrétisé lorsque j'ai commencé à travailler sur un système d'arbitrage. L'idée était simple : créer un système à haute fréquence pour détecter les déséquilibres de prix. À quoi tout cela a-t-il abouti en fin de compte ?
J'ai utilisé l’API de MetaTrader 5 le plus souvent au cours de cette période. J'ai décidé de calculer les taux de croisement synthétiques. J'ai décidé de ne pas me limiter à dix ou cent. Leur nombre a dépassé le millier.
La gestion des risques est une tâche distincte. Architecture du système, algorithmes, prise de décision - nous analyserons tout ici. Je montrerai les résultats du backtesting et du trading en direct. Et bien sûr, je partagerai des idées pour l'avenir. Qui sait, peut-être l'un d'entre vous souhaite-t-il approfondir ce sujet ? J'espère que mon travail sera demandé. J'aimerais croire qu'il contribuera au développement du commerce algorithmique. Peut-être que quelqu'un s'en inspirera pour créer quelque chose d'encore plus efficace dans le monde de l'arbitrage à haute fréquence. Après tout, c'est l'essence même de la science : avancer en s'appuyant sur l'expérience des prédécesseurs. Allons droit au but.
Introduction à l'arbitrage sur le marché des changes
Voyons ce qu'il en est vraiment.
Une analogie peut être faite avec le change de devises. Supposons que vous puissiez acheter des USD contre des EUR à un endroit, les vendre immédiatement contre des GBP à un autre endroit, puis échanger à nouveau des GBP contre des EUR et réaliser un bénéfice. C'est l'arbitrage dans sa forme la plus simple.
Mais en fait, c'est un peu plus compliqué. Le Forex est un marché énorme et décentralisé. On y trouve un grand nombre de banques, de courtiers et de fonds. Et chacun a son propre taux de change. Le plus souvent, ils ne correspondent pas. C'est là que nous avons une possibilité d'arbitrage. Mais ne pensez pas que c'est de l'argent facile. En général, ces écarts de prix ne durent que quelques secondes. Ou même des millisecondes. Il est presque impossible d'arriver à temps. Cela nécessite des ordinateurs puissants et des algorithmes rapides.
Il existe également différents types d'arbitrage. Un exemple simple est celui des bénéfices réalisés sur les différences de taux d'un endroit à l'autre. Un cas complexe est celui des taux croisés. Par exemple, nous calculons le coût de la livre sterling en dollars et en euros et le comparons au taux de change direct entre la livre sterling et l'euro.
La liste ne s'arrête pas là. Il y a aussi l'arbitrage temporel. Nous profitons ici de la différence de prix à différents moments. Acheté maintenant, vendu en une minute. Bien sûr, le processus semble simple. Mais le principal problème est que nous ne savons pas où ira le prix dans une minute. Tels sont les principaux risques. Le marché peut s'inverser plus rapidement que vous ne pouvez activer l'ordre souhaité. Ou votre courtier peut retarder l'exécution des ordres. En général, les difficultés et les risques sont assez nombreux. Malgré toutes les difficultés, l'arbitrage sur le Forex est un système assez populaire. Il s'agit de ressources financières importantes et d'un nombre suffisant de traders qui se spécialisent uniquement dans ce type d'opérations.
Après cette brève introduction, venons-en à notre stratégie.Aperçu des technologies utilisées : Python et MetaTrader 5
Donc, Python et MetaTrader 5.
Python est un langage de programmation polyvalent et facile à comprendre. Ce n'est pas pour rien qu'il est préféré par les développeurs novices et expérimentés. Il est le mieux adapté à l'analyse des données.
D'autre part, MetaTrader 5. Il s'agit d'une plateforme familière à tous les traders de Forex. Elle est fiable et n'est pas compliquée. Elle est également très fonctionnelle : cotations en temps réel, robots de trading et analyse technique. Une seule application. Pour obtenir des résultats positifs, nous devons combiner tous ces éléments.
Python récupère les données de MetaTrader 5, les traite à l'aide de ses bibliothèques, puis renvoie des ordres à MetaTrader 5 pour exécuter des transactions. Bien sûr, il y a des difficultés. Mais ensemble, ces applications sont très efficaces.
Une bibliothèque spéciale des développeurs est disponible pour travailler avec MetaTrader 5 à partir de Python. Pour l'activer, il suffit de l'installer. Après cela, nous sommes en mesure de recevoir des cotations, d'envoyer des ordres et de gérer des positions. Tout se passe de la même manière que dans le terminal lui-même, sauf que les capacités de Python sont maintenant utilisées.
Quelles sont les fonctionnalités et les capacités dont nous disposons aujourd'hui ? Il y en a beaucoup aujourd'hui. Par exemple, nous sommes en mesure d'automatiser les transactions et d'effectuer des analyses complexes de données historiques. Nous pouvons même créer notre propre plateforme de trading. Il s'agit déjà d'une tâche pour les utilisateurs avancés, mais c'est également possible.
Mise en place de l'environnement : installation des bibliothèques nécessaires et connexion à MetaTrader 5
Nous commencerons notre travail avec Python. Si vous ne l'avez pas encore, visitez python.org. Vous devez également définir le consentement ADD TO PATCH.
L'étape suivante est celle des bibliothèques. Nous aurons besoin de quelques-uns d'entre elles. La principale est MetaTrader 5. L'installation ne nécessite pas de compétences particulières.
Ouvrez la ligne de commande et tapez
pip install MetaTrader5 pandas numpy
Appuyez sur Entrée et allez boire un café. Ou du thé. Ou ce que vous préférez.
Tout est prêt ? Il est maintenant temps de se connecter à MetaTrader 5.
La première chose à faire est d'installer MetaTrader 5. Téléchargez-le à partir de votre courtier. N'oubliez pas le chemin d'accès au terminal. En règle générale, il se présente comme : "C:\ProgramFiles\MetaTrader 5\terminal64.exe".
Ouvrez ensuite Python et tapez :
import MetaTrader5 as mt5 if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"): print("Alas! Failed to connect :(") mt5.shutdown() else: print("Hooray! Connection successful!")
Si tout démarre, passez à la partie suivante.
Structure du code : principales fonctions et objectifs
Commençons par les "import". Nous avons ici des imports, tels que : MetaTrader5, pandas, datetime, pytz... Ensuite, il y a les fonctions.
- La première fonction est remove_duplicate_indices. Elle s'assure qu'il n'y a pas de doublons dans nos données.
- Vient ensuite get_mt5_data. Elle accède aux fonctions de MetaTrader 5 et extrait les données requises pour les dernières 24 heures.
- get_currency_data — une fonction très intéressante. Elle appelle get_mt5_data pour un certain nombre de paires de devises. AUDUSD, EURUSD, GBPJPY et bien d'autres paires.
- La suivante est calculate_synthetic_prices. Cette fonction est une véritable réussite. Elle produit des centaines de prix synthétiques tout en gérant les paires de devises.
- analyze_arbitrage recherche des opportunités d'arbitrage en comparant des prix réels à des prix synthétiques. Tous les résultats sont enregistrés dans un fichier CSV.
- open_test_limit_order — une autre unité de code puissante. Lorsqu'une opportunité d'arbitrage est trouvée, cette fonction ouvre un ordre de test. Mais pas plus de 10 transactions ouvertes en même temps.
Et enfin, la fonction "main". Elle gère l'ensemble du processus en appelant les fonctions dans le bon ordre.
Tout cela se termine par une boucle sans fin. Elle exécute la boucle complète toutes les 5 minutes, mais uniquement pendant les heures de travail. C'est la structure dont nous disposons. Elle est simple, mais efficace.
Obtenir des données de MetaTrader 5 : fonction get_mt5_data
La première tâche consiste à recevoir des données du terminal.
if not mt5.initialize(path=terminal_path): print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}") return None timezone = pytz.timezone("Etc/UTC") utc_from = datetime.now(timezone) - timedelta(days=1)
Notez que nous utilisons l'UTC. Car dans le monde du Forex, il n'y a pas de place pour la confusion des fuseaux horaires.
Le plus important est maintenant de récupérer les ticks :
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL) Les données ont été reçues ? Parfait ! Nous devons maintenant nous en occuper. Pour ce faire, nous utilisons pandas :
ticks_frame = pd.DataFrame(ticks) ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')
Voilà ! Nous avons maintenant notre propre DataFrame avec des données. Il est déjà préparé pour l'analyse.
Mais que se passe-t-il en cas de problème ? Ne vous inquiétez pas ! Notre fonction couvre également ce point :
if ticks is None: print(f"Failed to fetch data for {symbol}") return None
Elle se contentera de signaler un problème et de renvoyer None.
Gestion de plusieurs paires de devises : fonction get_currency_data
Nous pénétrons plus avant dans le système - la fonction get_currency_data. Jetons un coup d'œil au code :
def get_currency_data(): # Define currency pairs and the amount of data symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"] count = 1000 # number of data points for each currency pair data = {} for symbol in symbols: df = get_mt5_data(symbol, count, terminal_path) if df is not None: data[symbol] = df[['time', 'bid', 'ask']].set_index('time') return data
Tout commence par la définition des paires de devises. La liste comprend AUDUSD, EURUSD, GBPJPY et d'autres instruments que nous connaissons bien.
Nous passons maintenant à l'étape suivante. La fonction crée un dictionnaire "data" vide. Elle sera également complétée ultérieurement avec les données nécessaires.
Notre fonction commence maintenant son travail. Elle parcourt la liste des paires de devises. Pour chaque paire, elle appelle get_mt5_data. Si get_mt5_data renvoie des données (et non None), notre fonction ne prend que les plus importantes : time, bid et ask.
Et voici enfin le grand final. La fonction renvoie un dictionnaire rempli de données.
Nous obtenons maintenant get_currency_data. Elle est petite, puissante, simple mais efficace.
Calcul des 2000 prix synthétiques : Stratégie et mise en œuvre
Nous nous penchons sur les bases de notre système - la fonction calculate_synthetic_prices. Elle nous permet d'obtenir nos données synthétiques.
Jetons un coup d'œil au code :
def calculate_synthetic_prices(data):
synthetic_prices = {}
# Remove duplicate indices from all DataFrames in the data dictionary
for key in data:
data[key] = remove_duplicate_indices(data[key])
# Calculate synthetic prices for all pairs using multiple methods
pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]
method_count = 1
for pair1, pair2 in pairs:
print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['ask']
method_count += 1
print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['bid']
method_count += 1
return pd.DataFrame(synthetic_prices)
Analyse des possibilités d'arbitrage : fonction analyze_arbitrage
Tout d'abord, nous créons un dictionnaire vide synthetic_prices. Nous le remplirons également de données. Ensuite, nous passerons en revue toutes les données et supprimerons les indices en double afin d'éviter les erreurs à l'avenir.
L'étape suivante est la liste des "paires". Ce sont les paires de devises que nous utiliserons pour la synthèse. Un autre processus s'enclenche alors. Nous exécutons une boucle sur toutes les paires. Pour chaque paire, nous calculons le prix synthétique de deux manières :
- Diviser l'offre de la première paire par la demande de la seconde.
- Diviser l'offre de la première paire par l'offre de la seconde.
Chaque fois que nous augmentons notre nombre de méthodes. Nous obtenons ainsi 2000 paires synthétiques !
Voici comment fonctionne la fonction calculate_synthetic_prices. Elle ne se contente pas de calculer les prix, elle crée de nouvelles opportunités. Cette caractéristique permet d'obtenir d'excellents résultats sous la forme d'opportunités d'arbitrage !
Visualisation des résultats : Enregistrement des données au format CSV
Examinons la fonction analyze_arbitrage. Elle ne se contente pas d'analyser les données, elle recherche ce dont elle a besoin dans un flux de chiffres. Jetons un coup d'œil :
def analyze_arbitrage(data, synthetic_prices, method_count): # Calculate spreads for each pair spreads = {} for pair in data.keys(): for i in range(1, method_count + 1): synthetic_pair = f'{pair}_{i}' if synthetic_pair in synthetic_prices.columns: print(f"Analyzing arbitrage opportunity for {synthetic_pair}") spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair] # Identify arbitrage opportunities arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008 print("Arbitrage opportunities:") print(arbitrage_opportunities) # Save the full table of arbitrage opportunities to a CSV file arbitrage_opportunities.to_csv('arbitrage_opportunities.csv') return arbitrage_opportunities
Tout d'abord, notre fonction crée un dictionnaire "spreads" vide. Nous le remplirons également de données.
Passons à l'étape suivante. La fonction passe en revue toutes les paires de devises et leurs analogues synthétiques. Pour chaque paire, elle calcule l'écart - la différence entre le prix d'achat réel et le prix synthétique.
spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]
Cette ligne joue un rôle assez important. Elle trouve la différence entre le prix réel et le prix synthétique. Si cette différence est positive, nous avons une opportunité d'arbitrage.
Pour obtenir des résultats plus sérieux, nous utilisons le chiffre de 0,00008 :
arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008 Cette chaîne élimine toutes les possibilités inférieures à 8 points. De cette manière, nous obtiendrons des opportunités avec une plus grande probabilité de profit.
Voici l'étape suivante :
arbitrage_opportunities.to_csv('arbitrage_opportunities.csv') Toutes nos données sont maintenant enregistrées dans un fichier CSV. Nous pouvons maintenant les étudier, les analyser, tracer des graphiques - en général, faire un travail productif. Tout cela est possible grâce à la fonction suivante - analyze_arbitrage. Elle ne se contente pas d'analyser, elle recherche, trouve et sauvegarde les opportunités d'arbitrage.
Ouverture des ordres de test : fonction open_test_limit_order
Examinons ensuite la fonction open_test_limit_order. Elle ouvrira nos ordres pour nous.
Jetons un coup d'œil :
def open_test_limit_order(symbol, order_type, price, volume, take_profit, stop_loss, terminal_path): if not mt5.initialize(path=terminal_path): print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}") return None symbol_info = mt5.symbol_info(symbol) positions_total = mt5.positions_total() if symbol_info is None: print(f"Instrument not found: {symbol}") return None if positions_total >= MAX_OPEN_TRADES: print("MAX POSITIONS TOTAL!") return None # Check if symbol_info is None before accessing its attributes if symbol_info is not None: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "deviation": 30, "magic": 123456, "comment": "Stochastic Stupi Sustem", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, "tp": price + take_profit * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price - take_profit * symbol_info.point, "sl": price - stop_loss * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price + stop_loss * symbol_info.point, } result = mt5.order_send(request) if result is not None and result.retcode == mt5.TRADE_RETCODE_DONE: print(f"Test limit order placed for {symbol}") return result.order else: print(f"Error: Test limit order not placed for {symbol}, retcode={result.retcode if result is not None else 'None'}") return None else: print(f"Error: Symbol info not found for {symbol}") return None
La première chose que fait notre fonction est d'essayer de se connecter au terminal MetaTrader 5. Elle vérifie ensuite si l'instrument que nous voulons négocier existe.
Le code suivant :
if positions_total >= MAX_OPEN_TRADES: print("MAX POSITIONS TOTAL!") return None
Cette vérification permet de s'assurer que nous n'ouvrons pas trop de positions.
L'étape suivante consiste à générer la demande d'ouverture d'un ordre. Les paramètres sont assez nombreux. Type d'ordre, volume, prix, écart, nombre magique, commentaire... Si tout se passe bien, la fonction nous en informe. Si ce n'est pas le cas, le message apparaît.
Voici comment fonctionne la fonction open_test_limit_order. C'est notre lien avec le marché. D'une certaine manière, elle remplit les fonctions d'un courtier.
Restrictions temporaires du trading : travail pendant certaines heures
Parlons maintenant des heures d'ouverture.
if current_time >= datetime.strptime("23:30", "%H:%M").time() or current_time <= datetime.strptime("05:00", "%H:%M").time(): print("Current time is between 23:30 and 05:00. Skipping execution.") time.sleep(300) # Wait for 5 minutes before checking again continue
Que se passe-t-il ici ? Notre système vérifie l'heure. Si l'horloge affiche entre 23h30 et 5h00 du matin, il constate qu'il ne s'agit pas d'heures d'ouverture et se met en veille pendant 5 minutes. Ensuite, il s'active, vérifie à nouveau l'heure et, s'il est toujours en avance, se remet en mode veille.
Pourquoi avons-nous besoin de cela ? Il y a des raisons à cela. Premièrement, la liquidité. La nuit, il y en a généralement moins. Deuxièmement, les écarts. La nuit, ils s'étendent. Troisièmement, les informations. Les plus importantes d'entre elles apparaissent généralement pendant les heures de travail.
Boucle d'exécution et gestion des erreurs
Examinons la fonction "main". C'est comme un capitaine de navire, mais à la place du volant, il y a un clavier. Qu'est ce que ce code fait ? Tout est simple :
- Collecte de données
- Calcul des prix synthétiques
- Recherche d'opportunités d'arbitrage
- Ouverture des ordres
Il y a également une petite gestion des erreurs.
def main():
data = get_currency_data()
synthetic_prices = calculate_synthetic_prices(data)
method_count = 2000 # Define the method_count variable here
arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices, method_count)
# Trade based on arbitrage opportunities
for symbol in arbitrage_opportunities.columns:
if arbitrage_opportunities[symbol].any():
direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
symbol = symbol.split('_')[0] # Remove the index from the symbol
symbol_info = mt5.symbol_info_tick(symbol)
if symbol_info is not None:
price = symbol_info.bid if direction == "BUY" else symbol_info.ask
take_profit = 450
stop_loss = 200
order = open_test_limit_order(symbol, mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL, price, 0.50, take_profit, stop_loss, terminal_path)
else:
print(f"Error: Symbol info tick not found for {symbol}")
Évolutivité du système : Ajout de nouvelles paires de devises et de nouvelles méthodes
Vous souhaitez ajouter une nouvelle paire de devises ? Il suffit de l'ajouter à cette liste :
symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"]
Le système est maintenant au courant de l'existence de la nouvelle paire. . Qu'en est-il des nouvelles méthodes de calcul ?
def calculate_synthetic_prices(data):
# ... existing code ...
# Add a new method
synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['ask'] / data[pair2]['bid']
method_count += 1
Test et backtesting du système d'arbitrage
Parlons du backtesting. Il s'agit d'un point très important pour tout système de trading. Notre système d'arbitrage ne fait pas exception.
Qu'avons-nous fait ? Nous avons testé notre stratégie sur la base de données historiques. Mais pourquoi ? Pour comprendre son efficacité. Notre code commence par get_historical_data. Cette fonction récupère les anciennes données de MetaTrader 5. Sans ces données, nous ne pourrons pas travailler de manière productive.
Vient ensuite calculate_synthetic_prices. Nous calculons ici des taux de change synthétiques. Il s'agit d'un élément clé de notre stratégie d'arbitrage. Analyze_arbitrage est notre détecteur d'opportunités. Elle compare les prix réels avec les prix synthétiques et trouve la différence, ce qui nous permet d'obtenir un profit potentiel. simulate_trade est presque un processus de trading. Cependant, il se produit en mode test. Il s'agit d'un processus très important : il vaut mieux faire une erreur dans la simulation que de perdre de l'argent réel.
Enfin, backtest_arbitrage_system met tout cela ensemble et exécute notre stratégie sur des données historiques. Jour après jour, marché après marché.
import MetaTrader5 as mt5 import pandas as pd import numpy as np import matplotlib.pyplot as plt from datetime import datetime, timedelta import pytz # Path to MetaTrader 5 terminal terminal_path = "C:/Program Files/ForexBroker - MetaTrader 5/Arima/terminal64.exe" def remove_duplicate_indices(df): """Removes duplicate indices, keeping only the first row with a unique index.""" return df[~df.index.duplicated(keep='first')] def get_historical_data(start_date, end_date, terminal_path): if not mt5.initialize(path=terminal_path): print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}") return None symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"] historical_data = {} for symbol in symbols: timeframe = mt5.TIMEFRAME_M1 rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) if rates is not None and len(rates) > 0: df = pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') df.set_index('time', inplace=True) df = df[['open', 'high', 'low', 'close']] df['bid'] = df['close'] # Simplification: use 'close' as 'bid' df['ask'] = df['close'] + 0.000001 # Simplification: add spread historical_data[symbol] = df mt5.shutdown() return historical_data def calculate_synthetic_prices(data): synthetic_prices = {} pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'), ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'), ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'), ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'), ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'), ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'), ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'), ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'), ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'), ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'), ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')] for pair1, pair2 in pairs: if pair1 in data and pair2 in data: synthetic_prices[f'{pair1}_{pair2}_1'] = data[pair1]['bid'] / data[pair2]['ask'] synthetic_prices[f'{pair1}_{pair2}_2'] = data[pair1]['bid'] / data[pair2]['bid'] return pd.DataFrame(synthetic_prices) def analyze_arbitrage(data, synthetic_prices): spreads = {} for pair in data.keys(): for synth_pair in synthetic_prices.columns: if pair in synth_pair: spreads[synth_pair] = data[pair]['bid'] - synthetic_prices[synth_pair] arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008 return arbitrage_opportunities def simulate_trade(data, direction, entry_price, take_profit, stop_loss): for i, row in data.iterrows(): current_price = row['bid'] if direction == "BUY" else row['ask'] if direction == "BUY": if current_price >= entry_price + take_profit: return {'profit': take_profit * 800, 'duration': i} elif current_price <= entry_price - stop_loss: return {'profit': -stop_loss * 400, 'duration': i} else: # SELL if current_price <= entry_price - take_profit: return {'profit': take_profit * 800, 'duration': i} elif current_price >= entry_price + stop_loss: return {'profit': -stop_loss * 400, 'duration': i} # If the loop completes without hitting TP or SL, close at the last price last_price = data['bid'].iloc[-1] if direction == "BUY" else data['ask'].iloc[-1] profit = (last_price - entry_price) * 100000 if direction == "BUY" else (entry_price - last_price) * 100000 return {'profit': profit, 'duration': len(data)} def backtest_arbitrage_system(historical_data, start_date, end_date): equity_curve = [10000] # Starting with $10,000 trades = [] dates = pd.date_range(start=start_date, end=end_date, freq='D') for current_date in dates: print(f"Backtesting for date: {current_date.date()}") # Get data for the current day data = {symbol: df[df.index.date == current_date.date()] for symbol, df in historical_data.items()} # Skip if no data for the current day if all(df.empty for df in data.values()): continue synthetic_prices = calculate_synthetic_prices(data) arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices) # Simulate trades based on arbitrage opportunities for symbol in arbitrage_opportunities.columns: if arbitrage_opportunities[symbol].any(): direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL" base_symbol = symbol.split('_')[0] if base_symbol in data and not data[base_symbol].empty: price = data[base_symbol]['bid'].iloc[-1] if direction == "BUY" else data[base_symbol]['ask'].iloc[-1] take_profit = 800 * 0.00001 # Convert to price stop_loss = 400 * 0.00001 # Convert to price # Simulate trade trade_result = simulate_trade(data[base_symbol], direction, price, take_profit, stop_loss) trades.append(trade_result) # Update equity curve equity_curve.append(equity_curve[-1] + trade_result['profit']) return equity_curve, trades def main(): start_date = datetime(2024, 1, 1, tzinfo=pytz.UTC) end_date = datetime(2024, 8, 31, tzinfo=pytz.UTC) # Backtest for January-August 2024 print("Fetching historical data...") historical_data = get_historical_data(start_date, end_date, terminal_path) if historical_data is None: print("Failed to fetch historical data. Exiting.") return print("Starting backtest...") equity_curve, trades = backtest_arbitrage_system(historical_data, start_date, end_date) total_profit = sum(trade['profit'] for trade in trades) win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0 print(f"Backtest completed. Results:") print(f"Total Profit: ${total_profit:.2f}") print(f"Win Rate: {win_rate:.2%}") print(f"Final Equity: ${equity_curve[-1]:.2f}") # Plot equity curve plt.figure(figsize=(15, 10)) plt.plot(equity_curve) plt.title('Equity Curve: Backtest Results') plt.xlabel('Trade Number') plt.ylabel('Account Balance ($)') plt.savefig('equity_curve.png') plt.close() print("Equity curve saved as 'equity_curve.png'.") if __name__ == "__main__": main()
Pourquoi est-ce important ? Parce que le backtesting montre l'efficacité de notre système. Est-il rentable ou épuise-t-il votre dépôt ? De combien est le drawdown ? Quel est le pourcentage de transactions gagnantes ? Nous apprenons tout cela grâce au backtest.
Bien entendu, les résultats passés ne garantissent pas les résultats futurs. Le marché évolue. Mais sans backtest, nous n'obtiendrons aucun résultat. Connaissant le résultat, nous savons à peu près à quoi nous attendre. Autre point important : le backtesting permet d'optimiser le système. Nous modifions les paramètres et examinons le résultat encore et encore. Ainsi, étape par étape, nous améliorons notre système.
Voici le résultat du backtest de notre système :

Voici un test du système dans MetaTrader 5 :

Voici le code de l'EA MQL5 pour le système :
//+------------------------------------------------------------------+ //| TrissBotDemo.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" // Input parameters input int MAX_OPEN_TRADES = 10; input double VOLUME = 0.50; input int TAKE_PROFIT = 450; input int STOP_LOSS = 200; input double MIN_SPREAD = 0.00008; // Global variables string symbols[] = {"AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"}; int symbolsTotal; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { symbolsTotal = ArraySize(symbols); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Cleanup code here } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(!IsTradeAllowed()) return; datetime currentTime = TimeGMT(); if(currentTime >= StringToTime("23:30:00") || currentTime <= StringToTime("05:00:00")) { Print("Current time is between 23:30 and 05:00. Skipping execution."); return; } AnalyzeAndTrade(); } //+------------------------------------------------------------------+ //| Analyze arbitrage opportunities and trade | //+------------------------------------------------------------------+ void AnalyzeAndTrade() { double synthetic_prices[]; ArrayResize(synthetic_prices, symbolsTotal); for(int i = 0; i < symbolsTotal; i++) { synthetic_prices[i] = CalculateSyntheticPrice(symbols[i]); double currentPrice = SymbolInfoDouble(symbols[i], SYMBOL_BID); if(MathAbs(currentPrice - synthetic_prices[i]) > MIN_SPREAD) { if(currentPrice > synthetic_prices[i]) { OpenOrder(symbols[i], ORDER_TYPE_SELL); } else { OpenOrder(symbols[i], ORDER_TYPE_BUY); } } } } //+------------------------------------------------------------------+ //| Calculate synthetic price for a symbol | //+------------------------------------------------------------------+ double CalculateSyntheticPrice(string symbol) { // This is a simplified version. You need to implement the logic // to calculate synthetic prices based on your specific method return SymbolInfoDouble(symbol, SYMBOL_ASK); } //+------------------------------------------------------------------+ //| Open a new order | //+------------------------------------------------------------------+ void OpenOrder(string symbol, ENUM_ORDER_TYPE orderType) { if(PositionsTotal() >= MAX_OPEN_TRADES) { Print("MAX POSITIONS TOTAL!"); return; } double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double tp = (orderType == ORDER_TYPE_BUY) ? price + TAKE_PROFIT * point : price - TAKE_PROFIT * point; double sl = (orderType == ORDER_TYPE_BUY) ? price - STOP_LOSS * point : price + STOP_LOSS * point; MqlTradeRequest request = {}; MqlTradeResult result = {}; request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = VOLUME; request.type = orderType; request.price = price; request.deviation = 30; request.magic = 123456; request.comment = "ArbitrageAdvisor"; request.type_time = ORDER_TIME_GTC; request.type_filling = ORDER_FILLING_IOC; request.tp = tp; request.sl = sl; if(!OrderSend(request, result)) { Print("OrderSend error ", GetLastError()); return; } if(result.retcode == TRADE_RETCODE_DONE) { Print("Order placed successfully"); } else { Print("Order failed with retcode ", result.retcode); } } //+------------------------------------------------------------------+ //| Check if trading is allowed | //+------------------------------------------------------------------+ bool IsTradeAllowed() { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Print("Trade is not allowed in the terminal"); return false; } if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Print("Trade is not allowed in the Expert Advisor"); return false; } return true; }
Améliorations possibles et légalité du système pour les courtiers, ou comment ne pas frapper un fournisseur de liquidités avec des ordres limités.
Notre système présente d'autres difficultés potentielles. Les courtiers et les fournisseurs de liquidités désapprouvent souvent ces systèmes. Mais pourquoi ? Parce que nous retirons essentiellement les liquidités nécessaires du marché. Ils ont même créé un terme spécial pour cela : "Toxic Order Flow" (flux d’ordres toxiques).
Il s'agit d'un véritable problème. Nous aspirons littéralement la liquidité du système avec nos ordres de marché. Tout le monde en a besoin : les grands acteurs comme les petits traders. Bien sûr, cela a des conséquences.
Que faire dans cette situation ? Il existe un compromis : les ordres à cours limité.
Mais cela ne résout pas tous les problèmes : le label "flux d'ordres toxiques" n'est pas tant attribué en raison de l'absorption de la liquidité actuelle du marché qu'en raison des charges élevées liées à la gestion d'un tel flux d'ordres. Je n'ai pas encore résolu ce problème. Par exemple, il n'est pas rentable de consacrer 100 USD à la gestion d'un flux important de transactions d'arbitrage et de percevoir une commission de 50 USD. La clé est donc peut-être une rotation élevée et des lots de grande taille, ainsi qu'une vitesse de rotation élevée. Dans ce cas, les courtiers peuvent également être prêts à payer des rabais.
Passons maintenant au code. Comment pouvons-nous l'améliorer ? Tout d'abord, nous pouvons ajouter une fonction pour gérer les ordres limités. Il y a également beaucoup de travail ici - nous devons réfléchir à la logique de l'attente et de l'annulation des ordres non exécutés.
L'apprentissage automatique pourrait être une idée intéressante pour améliorer le système. Je suggère qu'il soit possible d'entraîner notre système à prédire les opportunités d'arbitrage qui ont le plus de chances de fonctionner.
Conclusion
Résumons. Nous avons créé un système qui recherche des opportunités d'arbitrage. N'oubliez pas que le système ne résout pas tous vos problèmes financiers.
Nous avons réglé la question du backtesting. Il fonctionne avec des données temporelles et, mieux encore, il nous permet de voir comment notre système aurait fonctionné dans le passé. Mais n'oubliez pas que les résultats passés ne garantissent pas les résultats futurs. Le marché est un mécanisme complexe en constante évolution.
Mais vous savez ce qui est le plus important ? Pas un code. Pas un algorithme. Mais vous. Votre désir d'apprendre, d'expérimenter, de faire des erreurs et de recommencer. C'est vraiment inestimable.
Ne vous arrêtez donc pas là. Ce système n'est que le début de votre voyage dans le monde du trading algorithmique. Utilisez-le comme point de départ pour de nouvelles idées et de nouvelles stratégies. Tout comme dans la vie, l'essentiel dans le trading est l'équilibre. L'équilibre entre le risque et la prudence, la cupidité et la rationalité, la complexité et la simplicité.
Bonne chance dans cette aventure passionnante, et que vos algorithmes aient toujours une longueur d'avance sur le marché !
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/15964
Avertissement: Tous les droits sur ces documents sont réservés par MetaQuotes Ltd. La copie ou la réimpression de ces documents, en tout ou en partie, est interdite.
Cet article a été rédigé par un utilisateur du site et reflète ses opinions personnelles. MetaQuotes Ltd n'est pas responsable de l'exactitude des informations présentées, ni des conséquences découlant de l'utilisation des solutions, stratégies ou recommandations décrites.
Comment Échanger des Données : Une DLL pour MQL5 en 10 minutes
Les méthodes de William Gann (Partie III) : L'astrologie fonctionne-t-elle ?
L'Histogramme des prix (Profile du Marché) et son implémentation en MQL5
Les méthodes de William Gann (Partie II) : Création de l'indicateur Carré de Gann (Gann Square)
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation
Veuillez expliquer de quoi il s'agit :
Voici les paires :
pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'), ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'), ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'), ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'), ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'), ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'), ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'), ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'), ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'), ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'), ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]Quelle est l'offre de la première paire ? La première paire est :
Quelle est l'offre de la première paire ? La première paire est :
AUDUSD est aussi une paire. AUD vers USD.
Veuillez expliquer de quoi il s'agit :
Voici les paires :
Quelle est l'offre de la première paire ? La première paire est :
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)Tout est installé. Voici ce qui apparaît dans les ticks :
array([b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'',
...
b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'',
b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'',
b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b''],
dtype='|V0')
Et ici, nous obtenons déjà une sortie à temps :
Le code de l'exemple https://www.mql5.com/fr/docs/python_metatrader5/mt5copyticksfrom_py ne fonctionne pas non plus
Bref, à quoi ressemble python ? Comment le préparer ? Ce n'est pas clair...