English Русский Español Português
preview
Hochfrequenz-Arbitrage-Handelssystem in Python mit MetaTrader 5

Hochfrequenz-Arbitrage-Handelssystem in Python mit MetaTrader 5

MetaTrader 5Handel | 15 Mai 2025, 08:46
72 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Einführung

Devisenmarkt. Algorithmische Strategien. Python und MetaTrader 5. Das kam zusammen, als ich begann, an einem Arbitrage-Handelssystem zu arbeiten. Die Idee war einfach - ein Hochfrequenzsystem zum Auffinden von Preisungleichgewichten zu schaffen. Wozu hat das alles letztendlich geführt?

In diesem Zeitraum habe ich am häufigsten MetaTrader 5 API verwendet. Ich habe beschlossen, synthetische Umtauschkurse zu berechnen. Ich habe beschlossen, mich nicht auf zehn oder hundert zu beschränken. Die Zahl hat die Tausendergrenze überschritten.

Das Risikomanagement war eine separate Aufgabe. Systemarchitektur, Algorithmen, Entscheidungsfindung - wir werden hier alles analysieren. Ich werde die Ergebnisse von Backtesting und Live-Handel zeigen. Und natürlich werde ich Ideen für die Zukunft mitteilen. Wer weiß, vielleicht hat ja jemand von Ihnen Lust, dieses Thema weiterzuentwickeln? Ich hoffe, dass meine Arbeit gefragt sein wird. Ich würde gerne glauben, dass es zur Entwicklung des algorithmischen Handels beitragen wird. Vielleicht wird sich jemand daran orientieren und etwas noch Effektiveres in der Welt der Hochfrequenz-Arbitrage schaffen. Schließlich ist das das Wesen der Wissenschaft - sich auf der Grundlage der Erfahrungen der Vorgänger weiterzuentwickeln. Kommen wir gleich zur Sache.


Einführung in den Forex-Arbitragehandel

Lassen Sie uns herausfinden, was es wirklich ist.

Es kann eine Analogie zum Währungsumtausch gezogen werden. Nehmen wir an, Sie können an einem Ort USD für EUR kaufen, sie an einem anderen Ort sofort für GBP verkaufen und dann GBP wieder in EUR umtauschen und am Ende einen Gewinn erzielen. Dies ist Arbitrage in ihrer einfachsten Form.

In Wirklichkeit ist es ein wenig komplizierter. Der Devisenmarkt ist ein riesiger, dezentraler Markt. Hier gibt es eine große Anzahl von Banken, Maklern und Fonds. Und jeder hat seine eigenen Wechselkurse. In den meisten Fällen stimmen sie nicht überein. Hier bietet sich eine Gelegenheit zur Arbitrage. Aber glauben Sie nicht, dass es sich um leicht verdientes Geld handelt. In der Regel dauern diese Preisunterschiede nur wenige Sekunden. Oder sogar Millisekunden. Es ist fast unmöglich, es noch rechtzeitig zu schaffen. Dies erfordert leistungsstarke Computer und schnelle Algorithmen.

Es gibt auch verschiedene Arten von Arbitrage. Ein einfaches Beispiel ist, wenn wir von den unterschiedlichen Tarifen an verschiedenen Orten profitieren. Ein komplexer Fall ist die Verwendung von Wechselkursen. Wir berechnen zum Beispiel, wie viel GBP in USD und EUR kostet, und vergleichen dies mit dem direkten GBP/EUR-Wechselkurs.

Die Liste ist damit noch nicht zu Ende. Es gibt auch eine Zeitarbitrage. Hier profitieren wir von der Differenz der Preise zu verschiedenen Zeitpunkten. Gekauft jetzt, verkauft eine Minute später. Natürlich scheint das Verfahren einfach zu sein. Aber das Hauptproblem ist, dass wir nicht wissen, wohin der Preis in einer Minute gehen wird. Dies sind die Hauptrisiken. Der Markt kann sich schneller umkehren, als Sie den gewünschten Auftrag aktivieren können. Oder Ihr Broker verzögert die Ausführung von Aufträgen. Im Allgemeinen gibt es eine ganze Reihe von Schwierigkeiten und Risiken. Trotz aller Schwierigkeiten ist die Forex-Arbitrage ein recht beliebtes System. Hier geht es um beträchtliche finanzielle Mittel und um genügend Händler, die sich nur auf diese Art des Handels spezialisiert haben.

Nach dieser kurzen Einführung wollen wir uns nun unserer Strategie zuwenden.


Überblick über die verwendeten Technologien: Python und MetaTrader 5

Also, Python und MetaTrader 5. 

Python ist eine vielseitige und leicht zu verstehende Programmiersprache. Nicht umsonst wird es sowohl von Anfängern als auch von erfahrenen Entwicklern bevorzugt. Und es ist bestens für die Datenanalyse geeignet.

Andererseits gibt es den MetaTrader 5. Diese Plattform ist jedem Forex-Händler vertraut. Es ist zuverlässig und nicht kompliziert. Und es ist auch ziemlich funktionell - Echtzeit-Kurse, Handelsroboter und technische Analysen. Alles in einer einzigen Anwendung. Um positive Ergebnisse zu erzielen, müssen wir all dies kombinieren.

Python übernimmt die Daten von MetaTrader 5, verarbeitet sie mithilfe seiner Bibliotheken und sendet dann Befehle zurück an MetaTrader 5, um Handelsgeschäfte auszuführen. Natürlich gibt es Schwierigkeiten. Aber zusammen sind diese Anwendungen sehr effizient.

Für die Arbeit mit MetaTrader 5 in Python steht eine spezielle Bibliothek der Entwickler zur Verfügung. Um sie zu aktivieren, müssen Sie es lediglich installieren. Danach sind wir in der Lage, Kurse zu empfangen, Aufträge zu senden und Positionen zu verwalten. Alles ist gleich wie im Terminal selbst, nur dass jetzt auch die Python-Fähigkeiten genutzt werden.

Welche Funktionen und Möglichkeiten stehen uns jetzt zur Verfügung? Davon gibt es inzwischen eine ganze Menge. So sind wir beispielsweise in der Lage, den Handel zu automatisieren und komplexe Analysen von historischen Daten durchzuführen. Wir können sogar unsere eigene Handelsplattform erstellen. Dies ist bereits eine Aufgabe für fortgeschrittene Nutzer, aber es ist auch möglich.


Einrichten der Umgebung: Installation der erforderlichen Bibliotheken und Verbindung zum MetaTrader 5

Wir werden unseren Arbeitsablauf mit Python beginnen. Wenn Sie es noch nicht haben, gehen Sie zu python.org. Sie müssen auch die Zustimmung zu ADD TO PATCH erteilen.

Unser nächster Schritt sind Bibliotheken. Wir werden ein paar von ihnen brauchen. Die wichtigste davon ist MetaTrader 5. Die Installation erfordert keine besonderen Kenntnisse.

Öffnen Sie die Befehlszeile und geben Sie ein:

pip install MetaTrader5 pandas numpy

Drücken Sie Enter und trinken Sie einen Kaffee. Oder Tee. Oder was immer Sie bevorzugen.

Ist alles vorbereitet? Nun ist es an der Zeit, eine Verbindung zum MetaTrader 5 herzustellen.

Als erstes müssen Sie MetaTrader 5 selbst installieren. Laden Sie ihn von Ihrem Broker herunter. Vergessen Sie nicht, den Pfad zum Terminal anzugeben. Normalerweise sieht das so aus: „C:\ProgramFiles\MetaTrader 5\terminal64.exe“.

Öffnen Sie nun Python und geben Sie ein:

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!")

Wenn alles startet, fahren Sie mit dem nächsten Teil fort.


Code-Struktur: Hauptfunktionen und ihr Zweck

Beginnen wir mit „imports“. Hier haben wir die Importe, wie zum Beispiel: MetaTrader5, pandas, datetime, pytz... Dann gibt es noch die Funktionen.

  • Die erste Funktion ist remove_duplicate_indices. Es stellt sicher, dass es keine Duplikate in unseren Daten gibt.
  • Als Nächstes kommt get_mt5_data. Es greift auf die Funktionen des MetaTrader 5 zu und extrahiert die erforderlichen Daten der letzten 24 Stunden.
  • get_currency_data - eine sehr interessante Funktion. Sie ruft get_mt5_data für eine Reihe von Währungspaaren auf. AUDUSD, EURUSD, GBPJPY und viele weitere Paare.
  • Die nächste ist calculate_synthetic_prices. Diese Funktion ist eine echte Errungenschaft. Es erzeugt Hunderte von synthetischen Preisen bei der Bearbeitung von Währungspaaren.
  • analyze_arbitrage sucht nach Arbitragemöglichkeiten, indem es reale Preise mit synthetischen Preisen vergleicht. Alle Ergebnisse werden in einer CSV-Datei gespeichert.
  • open_test_limit_order - eine weitere leistungsstarke Codeeinheit. Wenn eine Arbitragemöglichkeit gefunden wird, eröffnet diese Funktion einen Testauftrag. Aber nicht mehr als 10 offene Handelsgeschäfte zur gleichen Zeit.

Und schließlich die Funktion „main“. Es verwaltet diesen gesamten Prozess, indem es Funktionen in der richtigen Reihenfolge aufruft.

Das Ganze endet in einer Endlosschleife. Sie durchläuft die gesamte Schleife alle 5 Minuten, allerdings nur während der normalen Arbeitsstunden. Das ist die Struktur, die wir haben. Sie ist einfach, aber effizient. 


Abrufen von Daten aus MetaTrader 5: get_mt5_data Funktion

Die erste Aufgabe besteht darin, Daten vom Terminal zu empfangen.

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)

Beachten Sie, dass wir UTC verwenden. Denn in der Welt des Forex gibt es keinen Platz für Zeitzonenverwirrung.

Das Wichtigste ist jetzt, die Ticks zu bekommen:

ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)

Wurden die Daten empfangen? Großartig! Jetzt müssen wir damit etwas machen. Hierfür verwenden wir Pandas:

ticks_frame = pd.DataFrame(ticks)
ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')

Voilà! Jetzt haben wir unseren eigenen DataFrame mit Daten. Sie ist bereits für die Analyse vorbereitet.

Was aber, wenn etwas schief geht? Mach dir keine Sorgen! Unsere Funktion deckt auch dies ab:

if ticks is None:
    print(f"Failed to fetch data for {symbol}")
    return None

Sie meldet einfach ein Problem und gibt nichts zurück. So sieht unsere Funktion get_mt5_data aus. 


Handhabung mehrerer Währungspaare: die Funktion get_currency_data

Wir tauchen tiefer in das System ein - die Funktion get_currency_data. Werfen wir einen Blick auf den 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

Alles beginnt mit der Definition von Währungspaaren. Die Liste umfasst AUDUSD, EURUSD, GBPJPY und andere Instrumente, die uns gut bekannt sind.

Jetzt gehen wir zum nächsten Schritt über. Die Funktion erstellt ein Wörterbuch ohne Daten. Es wird später noch mit den erforderlichen Daten gefüllt werden.

Jetzt beginnt unsere Funktion mit ihrer Arbeit. Es wird die Liste der Währungspaare durchlaufen. Für jedes Paar ruft es get_mt5_data auf. Wenn get_mt5_data Daten zurückgibt (und nicht „nichts“), übernimmt unsere Funktion nur die wichtigsten: Zeit, Bid und Ask.

Und hier ist schließlich das große Finale. Die Funktion gibt ein Wörterbuch mit Daten zurück. 

Jetzt erhalten wir get_currency_data. Es ist klein, leistungsstark, einfach, aber effektiv.


Berechnung der synthetischen Preise für 2000: Strategie und Umsetzung

Wir tauchen ein in die Grundlagen unseres Systems - die Funktion calculate_synthetic_prices. So können wir unsere synthetisierten Daten erhalten.

Werfen wir einen Blick auf den 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 von Arbitragemöglichkeiten: die Funktion analyze_arbitrage

Zunächst erstellen wir das leeres Wörterbuch synthetic_prices. Wir werden es auch mit Daten füllen. Dann werden wir alle Daten durchgehen und doppelte Indizes entfernen, um Fehler in Zukunft zu vermeiden.

Der nächste Schritt ist die Liste der „Paare“. Dies sind die Währungspaare, die wir für die Synthese verwenden werden. Dann beginnt ein weiterer Prozess. Wir durchlaufen eine Schleife durch alle Paare. Für jedes Paar berechnen wir den synthetischen Preis auf zwei Arten:

  1. Teile den Bid-Preis des ersten Paares durch den Ask-Preis des zweiten Paares.
  2. Teile den Bid-Preis des ersten Paares durch den Bid-Preis des zweiten Paares.

Jedes Mal, wenn wir unseren method_count erhöhen. Als Ergebnis erhalten wir 2000 synthetische Paare!

So funktioniert die Funktion calculate_synthetic_prices. Sie berechnet nicht nur Preise, sondern schafft auch neue Möglichkeiten. Diese Funktion führt zu großartigen Ergebnissen in Form von Arbitragemöglichkeiten!


Visualisierung der Ergebnisse: Speichern der Daten in CSV

Schauen wir uns die Funktion analyze_arbitrage an. Sie analysiert nicht nur Daten, sondern sucht in einem Strom von Zahlen nach dem, was sie braucht. Schauen wir es uns an:

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

Zunächst erstellt unsere Funktion ein Wörterbuch für die „spreads“ ohne Werte. Wir werden es auch mit Daten füllen.

Lassen Sie uns zum nächsten Schritt übergehen. Die Funktion durchläuft alle Währungspaare und ihre synthetischen Entsprechungen. Für jedes Paar wird die Spanne berechnet - die Differenz zwischen dem realen Geldkurs und dem synthetischen Kurs.

spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]

Diese Saite spielt eine ziemlich wichtige Rolle. Sie ermittelt die Differenz zwischen dem realen und dem synthetischen Preis. Wenn diese Differenz positiv ist, haben wir eine Arbitragemöglichkeit.

Um seriösere Ergebnisse zu erhalten, verwenden wir die Zahl 0,00008:

arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008

Diese Zeichenfolge sortiert alle Möglichkeiten aus, die weniger als 8 Punkte haben. Auf diese Weise erhalten wir Gelegenheiten mit einer höheren Gewinnwahrscheinlichkeit.

Hier ist der nächste Schritt:

arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')

Jetzt sind alle unsere Daten in einer CSV-Datei gespeichert. Jetzt können wir sie studieren, analysieren, Charts erstellen - ganz allgemein: produktiv arbeiten. All dies wird durch die folgende Funktion ermöglicht: analyze_arbitrage. Sie analysiert nicht nur, sondern sucht, findet und sichert Arbitragemöglichkeiten.


Öffnen von Testaufträgen: die Funktion open_test_limit_order

Betrachten wir nun die Funktion open_test_limit_order. Sie wird unsere Aufträge für uns öffnen.

Schauen wir es uns an:

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

Als erstes versucht unsere Funktion, eine Verbindung zum MetaTrader 5-Terminal herzustellen. Dann wird geprüft, ob das Instrument, mit dem wir handeln wollen, überhaupt existiert.

Der folgende Code:

if positions_total >= MAX_OPEN_TRADES:
    print("MAX POSITIONS TOTAL!")
    return None

Diese Prüfung stellt sicher, dass wir nicht zu viele Positionen eröffnen.

Der nächste Schritt besteht darin, einen Antrag auf Eröffnung eines Auftrags zu stellen. Hier gibt es eine ganze Reihe von Parametern. Auftragsart, Volumen, Preis, Abweichung, magische Zahl, Kommentar... Wenn alles gut geht, teilt uns die Funktion dies mit. Wenn nicht, erscheint die Meldung.

So funktioniert die Funktion open_test_limit_order. Das ist unsere Verbindung zum Markt. In gewisser Weise übernimmt sie die Funktionen eines Maklers.


Vorübergehende Handelsbeschränkungen: die Arbeit während bestimmter Stunden

Lassen Sie uns nun über die Handelszeiten sprechen. 

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

Was ist hier eigentlich los? Unser System überprüft die Uhrzeit. Zeigt die Uhr zwischen 23:30 Uhr und 5:00 Uhr an, erkennt sie, dass dies keine Handelszeiten sind, und geht für 5 Minuten in den Standby-Modus. Dann schaltet es sich ein, prüft erneut die Uhrzeit und geht, falls es noch zu früh ist, wieder in den Standby-Modus.

Warum brauchen wir das? Hierfür gibt es Gründe. Erstens: Liquidität. In der Nacht ist sie meist geringer. Zweitens: Spreads. In der Nacht weiten sie sich. Drittens: Nachrichten. Die wichtigsten werden in der Regel während den normalen Arbeitsstunden veröffentlicht.


Laufzeitschleife und Fehlerbehandlung

Werfen wir einen Blick auf die Funktion „main“. Es ist wie bei einem Schiffskapitän, nur dass es statt eines Steuerrads eine Tastatur gibt. Was macht sie? Ist alles ganz einfach:

  1. Datenerhebung
  2. Berechnung der synthetischen Preise 
  3. Suche nach Arbitragemöglichkeiten 
  4. Eröffnung von Aufträgen

Es gibt auch eine kleine Fehlerbehandlung. 

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}")


Skalierbarkeit des Systems: Hinzufügen neuer Währungspaare und Methoden

Möchten Sie ein neues Währungspaar hinzufügen? Nehmen Sie sie einfach in diese Liste auf:

symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"]

Das System kennt nun das neue Paar. . Was ist mit den neuen Berechnungsmethoden? 

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


Testen und Backtesting des Arbitrage-Systems

Lassen Sie uns über Backtesting sprechen. Dies ist ein wirklich wichtiger Punkt für jedes Handelssystem. Unser Arbitrage-System ist da keine Ausnahme.

Was haben wir getan? Wir haben unsere Strategie anhand historischer Daten überprüft. Warum? Um zu verstehen, wie effizient sie ist. Unser Code beginnt mit get_historical_data. Diese Funktion ruft alte Daten aus MetaTrader 5 ab. Ohne diese Daten sind wir nicht in der Lage, produktiv zu arbeiten.

Dann kommt calculate_synthetic_prices. Hier berechnen wir synthetische Wechselkurse. Dies ist ein wesentlicher Bestandteil unserer Arbitragestrategie. Analyze_arbitrage ist unser Chancen-Detektor. Er vergleicht reale Preise mit synthetischen Preisen und ermittelt die Differenz, sodass wir potenzielle Gewinne erzielen können. simulate_trade ist fast ein Handelsprozess. Es tritt jedoch im Testmodus auf. Dies ist ein sehr wichtiger Prozess: Es ist besser, in der Simulation einen Fehler zu machen, als echtes Geld zu verlieren.

Backtest_arbitrage_system schließlich fasst alles zusammen und führt unsere Strategie anhand historischer Daten aus. Tag für Tag, Handelsgeschäft für Handelsgeschäft.

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()

Warum ist das wichtig? Denn das Backtesting zeigt, wie effizient unser System ist. Ist es gewinnbringend oder zehrt es Ihre Einlage auf? Was ist ein Drawdown? Wie hoch ist der Prozentsatz der Abschlüsse mi Gewinn? All dies erfahren wir aus dem Backtest.

Natürlich sind die Ergebnisse der Vergangenheit keine Garantie für zukünftige Ergebnisse. Der Markt verändert sich. Aber ohne einen Backtest werden wir keine Ergebnisse erhalten. Da wir das Ergebnis kennen, wissen wir ungefähr, was uns erwartet. Ein weiterer wichtiger Punkt: Backtesting hilft bei der Optimierung des Systems. Wir ändern die Parameter und sehen uns das Ergebnis immer wieder an. So verbessern wir Schritt für Schritt unser System.

Hier ist das Ergebnis des Backtests unseres Systems:

Hier ist ein Test des Systems in MetaTrader 5:

Und hier ist der Code des MQL5 EA für das System:

//+------------------------------------------------------------------+
//|                                                 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;
}


Mögliche Verbesserungen und die Rechtmäßigkeit des Systems für Makler oder wie man einen Liquiditätsanbieter nicht mit Limit-Aufträgen überfällt

Unser System birgt weitere potenzielle Schwierigkeiten. Makler und Liquiditätsanbieter missbilligen solche Systeme häufig. Warum? Denn wir entziehen dem Markt im Wesentlichen die notwendige Liquidität. Es wurde sogar ein spezieller Begriff dafür erfunden - Toxic Order Flow. 

Dies ist ein echtes Problem. Mit unseren Marktaufträgen saugen wir buchstäblich Liquidität aus dem System ab. Jeder braucht sie: sowohl die großen Akteure als auch die kleinen Gewerbetreibenden. Das hat natürlich seine Folgen.

Was ist in dieser Situation zu tun? Es gibt einen Kompromiss: Limitaufträge. 

Damit sind jedoch nicht alle Probleme gelöst: Die Kennzeichnung „Toxic Order Flow“ erfolgt nicht so sehr wegen der Absorption der aktuellen Liquidität vom Markt, sondern wegen der hohen Belastungen bei der Bedienung eines solchen Auftragsstroms. Ich habe dieses Problem noch nicht gelöst. So ist es zum Beispiel unrentabel, 100 USD für die Bedienung einer großen Anzahl von Arbitrageur-Transaktionen auszugeben und dafür eine Provision von 50 USD zu erhalten. Der Schlüssel liegt hier also vielleicht in einem hohen Umsatz und einer hohen Losgröße sowie einer hohen Umsatzgeschwindigkeit. Dann könnten die Makler auch bereit sein, Rabatte zu zahlen.

Jetzt geht es an den Code. Wie können wir sie verbessern? Zunächst können wir eine Funktion zur Bearbeitung von Limit-Aufträgen hinzufügen. Auch hier gibt es eine Menge Arbeit - wir müssen die Logik des Wartens und der Stornierung nicht ausgeführter Aufträge durchdenken.

Maschinelles Lernen könnte eine interessante Idee zur Verbesserung des Systems sein. Ich schlage vor, dass es möglich ist, unser System so zu trainieren, dass es vorhersagen kann, welche Arbitragemöglichkeiten am ehesten funktionieren werden. 


Schlussfolgerung

Fassen wir es zusammen. Wir haben ein System entwickelt, das nach Arbitragemöglichkeiten sucht. Denken Sie daran, dass das System nicht alle Ihre finanziellen Probleme löst. 

Wir haben das Backtesting erledigt. Es funktioniert mit zeitbasierten Daten, und was noch besser ist, es ermöglicht uns zu sehen, wie unser System in der Vergangenheit funktioniert hätte. Aber denken Sie daran, dass die Ergebnisse der Vergangenheit keine Garantie für künftige Ergebnisse sind. Der Markt ist ein komplexer Mechanismus, der sich ständig verändert.

Aber wissen Sie, was das Wichtigste ist? Nicht der Code. Nicht die Algorithmen. Sondern Sie. Ihr Wunsch, zu lernen, zu experimentieren, Fehler zu machen und es erneut zu versuchen. Das ist wirklich unbezahlbar.

Bleiben Sie also nicht dabei stehen. Dieses System ist nur der Anfang Ihrer Reise in die Welt des algorithmischen Handels. Nutzen Sie sie als Ausgangspunkt für neue Ideen und neue Strategien. Genau wie im Leben ist das Wichtigste beim Handel das Gleichgewicht. Das Gleichgewicht zwischen Risiko und Vorsicht, Gier und Rationalität, Komplexität und Einfachheit.

Viel Glück auf dieser spannenden Reise, und mögen Ihre Algorithmen dem Markt immer einen Schritt voraus sein!

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/15964

Von der Grundstufe bis zur Mittelstufe: Prioritätsfolge der Operatoren Von der Grundstufe bis zur Mittelstufe: Prioritätsfolge der Operatoren
Dies ist sicherlich die schwierigste Frage, die sich rein theoretisch erklären lässt. Deshalb müssen Sie alles üben, was wir hier besprechen werden. Dies mag auf den ersten Blick einfach erscheinen, aber das Thema Operatoren kann nur in der Praxis in Verbindung mit ständiger Weiterbildung verstanden werden.
Atmosphere Clouds Model Optimization (ACMO): Die Praxis Atmosphere Clouds Model Optimization (ACMO): Die Praxis
In diesem Artikel werden wir uns weiter mit der Implementierung des ACMO-Algorithmus (Atmospheric Cloud Model Optimization) beschäftigen. Wir werden insbesondere zwei Schlüsselaspekte erörtern: die Bewegung von Wolken in Tiefdruckgebiete und die Regensimulation, einschließlich der Initialisierung von Tröpfchen und ihrer Verteilung auf die Wolken. Wir werden uns auch mit anderen Methoden befassen, die eine wichtige Rolle bei der Verwaltung des Zustands von Wolken und der Gewährleistung ihrer Interaktion mit der Umwelt spielen.
Artificial Showering Algorithm (ASHA) Artificial Showering Algorithm (ASHA)
Der Artikel stellt den Künstlichen Duschalgorithmus (ASHA) vor, eine neue metaheuristische Methode, die für die Lösung allgemeiner Optimierungsprobleme entwickelt wurde. Auf der Grundlage der Simulation von Wasserfluss- und Akkumulationsprozessen konstruiert dieser Algorithmus das Konzept eines idealen Feldes, in dem jede Einheit der Ressource (Wasser) aufgerufen ist, eine optimale Lösung zu finden. Wir werden herausfinden, wie ASHA Fließ- und Akkumulationsprinzipien anpasst, um Ressourcen in einem Suchraum effizient zuzuweisen, und seine Implementierung und Testergebnisse sehen.
Von der Grundstufe bis zur Mittelstufe: Die Anweisung FOR Von der Grundstufe bis zur Mittelstufe: Die Anweisung FOR
In diesem Artikel werden wir uns mit den grundlegenden Konzepten der FOR-Anweisung befassen. Es ist sehr wichtig, dass Sie alles verstehen, was hier gezeigt wird. Im Gegensatz zu den anderen Anweisungen, über die wir bisher gesprochen haben, hat die FOR-Anweisung einige Eigenheiten, die sie schnell sehr komplex machen. Lassen Sie also nicht zu, dass sich solche Dinge ansammeln. Beginnen Sie so bald wie möglich mit dem Lernen und Üben.