English
preview
Python-MetaTrader 5 Strategietester (Teil 04): Tester 101

Python-MetaTrader 5 Strategietester (Teil 04): Tester 101

MetaTrader 5Handelssysteme |
23 4
Omega J Msigwa
Omega J Msigwa

Inhalt


Einführung

In den vorangegangenen Artikeln dieser Serie haben wir die Grundlagen für den Aufbau eines MetaTrader 5-ähnlichen Strategietesters von Grund auf gelegt. Auch wenn die Kernstruktur vorhanden ist, fehlen in unserem Projekt noch einige wichtige Komponenten.

In diesem Stadium müssen wir noch Ticks und Balken sequenziell verarbeiten, es fehlen Mechanismen zur Überwachung offener Aufträge und des simulierten Handelskontos, und wir verfügen nicht über Leistungskennzahlen wie Gewinn und Verlust, Drawdown, Gewinnrate, Risiko-Ertrags-Verhältnisse und detaillierte Handelsstatistiken im Simulator.

Dieser Artikel zielt darauf ab, diese Lücken zu schließen und unser Projekt weiter zu verbessern.


Vom Simulator zum Tester

Wenn Sie auf die Klasse geachtet haben, an der wir in den vorherigen Beiträgen gearbeitet haben, nannten wir sie Simulator. Der Name sollte allen Nutzern einen einfachen Namen geben, den jeder verstehen kann. Der Strategietester des MetaTrader 5 ist in der Tat ein Simulator, sodass wir dieses Mal unseren Klassennamen in Tester ändern.

class Tester:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):

Diese Änderung geht mit einer Änderung der Ordnerstruktur für Protokolle einher.

class Tester:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):
        """MetaTrader 5-Like Strategy tester for the MetaTrader5-Python module.

        Args:
            simulator_name (str): A Bot or Simulator's name
            mt5_instance (mt5): An instance of the Initialized MetaTrader 5 module
            deposit (float): The initial account balance for the Tester
            leverage (_type_, optional): A leverage of the simulated account. Defaults to "1:100".

        Raises:
            RuntimeError: When one of the operation fails
        """
        
        self.mt5_instance = mt5_instance
        self.simulator_name = simulator_name
        
        config.mt5_logger = config.get_logger(self.simulator_name+".mt5", 
                                                 logfile=os.path.join(config.MT5_LOGS_DIR, f"{config.LOG_DATE}.log"), level=config.logging_level)
        
        config.tester_logger = config.get_logger(self.simulator_name+".tester", 
                                                    logfile=os.path.join(config.TESTER_LOGS_DIR, f"{config.LOG_DATE}.log"), level=config.logging_level)

Ausgabe:


Konfiguration und Initialisierung des Testers

Um zu verstehen, was für unseren maßgeschneiderten Strategietester von Anfang an erforderlich ist, müssen wir uns die Konfigurationen des Strategietesters in MetaTrader 5 ansehen.

Eröffnung Beschreibung und Verwendung
Experte Der Name eines Expert Advisors (EA) (Handelsroboters), den Sie testen möchten.
Symbol Dies ist das Chartsymbol, mit dem der EA verbunden ist. Auch wenn Ihr EA mit mehreren Symbolen handelt, wird dieses Symbol als „Host“-Chart benötigt.
Zeitrahmen – das Feld rechts neben dem Symbol Der vom EA verwendete Chart-Zeitrahmen. Der Zeitrahmen bestimmt, wie sich OnTick(), OnTimer() und Bar-Ereignisse verhalten.
Datum (Anfangsdatum) – erstes Feld Das erste historische Datum, das für den Test verwendet wird.
Datum (Enddatum) – zweites Feld Das letzte historische Datum, das für den Test verwendet wurde.
Vorwärts Vorwärts-Testperiode, nach der Test- oder Optimierungsperiode.
Verzögerungen  Simulation der Ausführungsverzögerung. Es gibt zwei Optionen:
  • Latenz null, ideale Ausführung. Perfekte Füllungen (Standard)
  • Nutzerdefinierte Verzögerung. Simuliert reale Broker-Latenzzeiten
Modellierung Legt fest, wie Ticks während des Tests erzeugt werden. Unterstützt werden u. a. folgende Modellierungen:
  • Jeder Tick
  • Jeder Tick anhand realer Ticks
  • Nur Eröffnungspreise
  • 1-Minuten-OHLC
  • Mathematische Berechnungen
Einzahlung  Kontostand bei Beginn 
Hebel  Hebel des Kontos, der im Test verwendet werden soll. 
Optimierung Modus der Parameteroptimierung. Wenn diese Funktion deaktiviert ist, wird ein einziger Test durchgeführt; wenn sie aktiviert ist, führt der Tester einen EA mehrmals mit verschiedenen Parameterkombinationen aus. 
Visueller Modus...  Zeigt während der Prüfung eine Animation des Charts an. 

Wir müssen ähnliche Konfigurationen an unsere Testerklasse übergeben. Eine JSON-Datei ist für unser konsolenbasiertes Python-Projekt praktisch.

configs/tester.json

{
    "tester": {
        "bot_name": "MY EA",
        "symbols": ["EURUSD", "USDCAD", "USDJPY"],
        "timeframe": "H1",
        "start_date": "01.01.2025 00:00",
        "end_date": "31.12.2025 00:00",
        "modelling" : "real_ticks",
        "deposit": 1000,
        "leverage": "1:100"
    }
}

Wir bernötigen erst einmal ein paar Parameter. Es gibt noch Raum für Verbesserungen, wenn wir tiefer in das Projekt einsteigen.

Die Einführung einer JSON-Datei für Konfigurationen macht einige der Argumente, die wir in der vorherigen Version dieser Klasse hatten, überflüssig. Nachstehend finden Sie einen neuen Klassenkonstruktor.

class Tester:
    def __init__(self, tester_config: dict, mt5_instance: mt5):
        """MetaTrader 5-Like Strategy tester for the MetaTrader5-Python module.

        Args:
            configs_json (dict): a dictonary containing tester configurations
        Raises:
            RuntimeError: When one of the operation fails
        """

In MetaTrader 5 müssen wir nur das Host-Symbol angeben; die Plattform selbst kümmert sich um die Importe und handhabt die Ticks für alle Instrumente, die im Programm eingesetzt werden, automatisch.

Es ist unklar, wie das Terminal dies tun kann, aber MQL5 ist eine kompilierungsbasierte Sprache, im Gegensatz zu Python; wir könnten es schwer haben, dies nachzuahmen. Zurzeit muss der Nutzer alle Instrumente angeben, die während der Programmlaufzeit verwendet werden sollen. 

"symbols": ["EURUSD", "USDCAD", "USDJPY"],

Wenn man sich jedoch auf eine JSON-Datei für die Parameter verlässt, ist die Gefahr von Fehlern sehr groß; selbst ein kleiner Tippfehler reicht aus, um das Programm zu zerstören. 

Deshalb bernötigen wir Funktionen zur Validierung der von einem solchen Objekt erhaltenen Informationen.

 I: Sicherstellen, dass wir gültige JSON-Schlüssel haben

Die nachstehende Funktion löst Laufzeitfehler aus, wenn in einem Wörterbuch entweder ein Parameter fehlt oder ein unbekannter (zusätzlicher) Parameter vorhanden ist.

Innerhalb von validators.py

class TesterConfigValidators:
    """
    Responsible for validating and normalizing strategy tester configurations.
    """

    def __init__(self):
        pass
    
    @staticmethod
    def _validate_keys(raw_config: Dict) -> None:
        required_keys = {
            "bot_name",
            "symbols",
            "timeframe",
            "start_date",
            "end_date",
            "modelling",
            "deposit",
            "leverage",
        }

        provided_keys = set(raw_config.keys())

        missing = required_keys - provided_keys
        if missing:
            raise RuntimeError(f"Missing tester config keys: {missing}")

        extra = provided_keys - required_keys
        if extra:
            raise RuntimeError(f"Unknown tester config keys: {extra}")

Jedes Mal, wenn wir Änderungen vornehmen, indem wir einen neuen Parameter in unsere JSON-Datei unter dem übergeordneten Schlüssel „tester“ einfügen, müssen wir auch ein Wörterbuch namens required_keys aktualisieren.

II: Prüfen, ob alle Tasten korrekte Begleitwerte haben

Wir müssen sicherstellen, dass für jeden Eintrag ein korrekter Datentyp angegeben wird.

validators.py

    @staticmethod
    def parse_tester_configs(raw_config: Dict) -> Dict:
        TesterConfigValidators._validate_keys(raw_config)

        cfg: Dict = {}

        # --- BOT NAME ---
        cfg["bot_name"] = str(raw_config["bot_name"])

        # --- SYMBOLS ---
        symbols = raw_config["symbols"]
        if not isinstance(symbols, list) or not symbols:
            raise RuntimeError("symbols must be a non-empty list")
        cfg["symbols"] = symbols

        # --- TIMEFRAME ---
        timeframe = raw_config["timeframe"].upper()
        if timeframe not in utils.TIMEFRAMES:
            raise RuntimeError(f"Invalid timeframe: {timeframe}")
        cfg["timeframe"] = timeframe

        # --- MODELLING ---
        modelling = raw_config["modelling"].lower()
        VALID_MODELLING = {"real_ticks", "new_bar"}
        if modelling not in VALID_MODELLING:
            raise RuntimeError(f"Invalid modelling mode: {modelling}")
        cfg["modelling"] = modelling

        # --- DATE PARSING ---
        try:
            start_date = datetime.strptime(
                raw_config["start_date"], "%d.%m.%Y %H:%M"
            )
            end_date = datetime.strptime(
                raw_config["end_date"], "%d.%m.%Y %H:%M"
            )
        except ValueError:
            raise RuntimeError("Date format must be: DD.MM.YYYY HH:MM")

        if start_date >= end_date:
            raise RuntimeError("start_date must be earlier than end_date")

        cfg["start_date"] = start_date
        cfg["end_date"] = end_date

        # --- DEPOSIT ---
        deposit = float(raw_config["deposit"])
        if deposit <= 0:
            raise RuntimeError("deposit must be > 0")
        cfg["deposit"] = deposit

        # --- LEVERAGE ---
        cfg["leverage"] = TesterConfigValidators._parse_leverage(raw_config["leverage"])

        return cfg

Innerhalb eines Klassenkonstruktors wird ein aus einer JSON-Datei empfangenes Wörterbuch validiert, bevor das resultierende Wörterbuch einer Variablen zugewiesen wird, auf die die gesamte Klasse zugreifen kann.

self.tester_config = TesterConfigValidators.parse_tester_configs(tester_config)

Da wir nun Informationen aus der JSON-Datei abrufen können, wollen wir sehen, wie wir beim Testen unserer Handelsroboter mit verschiedenen Modellen umgehen können.


Strategietests auf der Grundlage von REALEN TICKS

Von allen im Strategietester des MetaTrader 5 verfügbaren Preismodellen ist dies das genaueste. Dabei wird ein Programm (ein Indikator oder ein Expert Advisor) mit Ticks getestet, die direkt von einem Broker bezogen werden.

Dies ist sehr einfach zu implementieren, wenn man bedenkt, dass wir bereits Funktionen eingeführt haben, um Ticks und Balken aus einem bestimmten Zeitraum in der Vergangenheit zu erhalten.

tester.py

from src import ticks
class Tester:
    def __init__(self, tester_config: dict, mt5_instance: mt5):
        
        # ...
        
        self.__GetLogger().info("Tester Initializing")
        self.__GetLogger().info(f"Tester configs: {self.tester_config}")
        
        self.TESTER_ALL_TICKS_INFO = [] # for storing all ticks to be used during the test
        self.TESTER_ALL_BARS_INFO = [] # for storing all bars to be used during the test
        
        start_dt = self.tester_config["start_date"]
        end_dt = self.tester_config["end_date"]
        
        modelling = self.tester_config["modelling"]
        
        for symbol in self.tester_config["symbols"]:
            
            if modelling == "real_ticks":
                    
                ticks_obtained = ticks.fetch_historical_ticks(start_datetime=start_dt, end_datetime=end_dt, symbol=symbol)
                
                ticks_info = {
                    "symbol": symbol,
                    "ticks": ticks_obtained,
                    "size": ticks_obtained.height,
                    "counter": 0
                }
                
                self.TESTER_ALL_TICKS_INFO.append(ticks_info)

Nachdem wir die Ticks von allen gegebenen Instrumenten erhalten haben, fügen wir das Ergebnis in ein Array namens TESTER_ALL_TICKS_INFO ein.  Dies ist die Schleife, die wir später in der Hauptsimulationsschleife durchlaufen werden.

In der Funktion OnTick in dieser Klasse fügen wir alles zusammen.

    def OnTick(self, ontick_func):
        """Calls the assigned function upon the receival of new tick(s)

        Args:
            ontick_func (_type_): A function to be called on every tick
        """
        
        if not self.IS_TESTER:
            return

        modelling = self.tester_config["modelling"]
        if modelling == "real_ticks":

            self.__GetLogger().debug(f"total number of ticks: {total_ticks}")

                while True:
                    
                    any_tick_processed = False

                    for ticks_info in self.TESTER_ALL_TICKS_INFO:

                        symbol = ticks_info["symbol"]
                        size = ticks_info["size"]
                        counter = ticks_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = ticks_info["ticks"].row(counter)

                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        ticks_info["counter"] = counter + 1
                        any_tick_processed = True

                    if not any_tick_processed:
                        break

 Ein Fortschrittsbalken ist in solchen Situationen sehr nützlich:

    def OnTick(self, ontick_func):
        """Calls the assigned function upon the receival of new tick(s)

        Args:
            ontick_func (_type_): A function to be called on every tick
        """
        
        if not self.IS_TESTER:
            return

        modelling = self.tester_config["modelling"]
        if modelling == "real_ticks" or modelling == "every_tick":
            
            total_ticks = sum(ticks_info["size"] for ticks_info in self.TESTER_ALL_TICKS_INFO)

            self.__GetLogger().debug(f"total number of ticks: {total_ticks}")

            with tqdm(total=total_ticks, desc="Tester Progress", unit="tick") as pbar:
                while True:
                    
                    any_tick_processed = False

                    for ticks_info in self.TESTER_ALL_TICKS_INFO:

                        symbol = ticks_info["symbol"]
                        size = ticks_info["size"]
                        counter = ticks_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = ticks_info["ticks"].row(counter)

                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        ticks_info["counter"] = counter + 1
                        any_tick_processed = True

                        pbar.update(1)

                    if not any_tick_processed:
                        break

Die obige Methode namens OnTick nimmt eine gegebene Funktion, vermutlich eine Hauptfunktion für den Handel, genau wie die Funktion OnTick in MQL5.

Eine empfangene Funktion aus dem Argument ontick_func wird innerhalb der OnTick-Methode bei jeder Tick-Iteration in der Hauptsimulationsschleife aufgerufen.

Beispiel für die Verwendung:

tester.py

if __name__ == "__main__":
    
    mt5.initialize()
    
    try:
        with open(os.path.join('configs/tester.json'), 'r', encoding='utf-8') as file: # reading a JSON file
            # Deserialize the file data into a Python object
            data = json.load(file)
    except Exception as e:
        raise RuntimeError(e)

    sim = Tester(tester_config=data["tester"], mt5_instance=mt5)

    def ontick_function():
        pass
        # print("some trading actions")

    sim.OnTick(ontick_function)

Ausgabe:


Strategieprüfung auf der Grundlage von SIMULATED TICKS

Wenn die Modellierung im Strategietester von MetaTrader 5 auf jeden Tick eingestellt ist, verwendet das Terminal synthetische Ticks. Erzeugt durch eine Art von Algorithmus, der in diesem Artikel besprochen wird.

Ein Versuch, diesen Algorithmus nachzubilden, findet sich in einer Klasse in src/ticks_gen.py.

Dieses Mal lesen wir die Ticks nicht aus einem Polars-Dataframe, sondern generieren die Ticks anhand von Ein-Minuten-Balken.

Innerhalb von tester.py

from src.ticks_gen import TicksGen
elif modelling == "every_tick":
                
    bars_df = bars.fetch_historical_bars(symbol=symbol, 
                                        timeframe=utils.TIMEFRAMES["M1"], 
                                        start_datetime=start_dt, end_datetime=end_dt)
                
    ticks_obtained = TicksGen.generate_ticks_from_bars(bars=bars_df, symbol=symbol, 
                                                    symbol_point=self.symbol_info(symbol).point,
                                                    out_dir=f"{config.SIMULATED_TICKS_DIR}/{symbol}", 
                                                    return_df=True)
                
    ticks_info = {
        "symbol": symbol,
        "ticks": ticks_obtained,
        "size": ticks_obtained.height,
        "counter": 0
    }
                
    self.TESTER_ALL_TICKS_INFO.append(ticks_info)

Der einzige Unterschied zwischen echten und generierten Ticks besteht darin, dass der eine generiert wird, während der andere aus einer Datenbank extrahiert wird; alles andere ist gleich, sodass wir sie in der OnTick-Methode ähnlich behandeln. 

    def OnTick(self, ontick_func):
        """Calls the assigned function upon the receival of new tick(s)

        Args:
            ontick_func (_type_): A function to be called on every tick
        """
        
        if not self.IS_TESTER:
            return

        modelling = self.tester_config["modelling"]
        if modelling == "real_ticks" or modelling == "every_tick":
            
            total_ticks = sum(ticks_info["size"] for ticks_info in self.TESTER_ALL_TICKS_INFO)

            self.__GetLogger().debug(f"total number of ticks: {total_ticks}")

            with tqdm(total=total_ticks, desc="Tester Progress", unit="tick") as pbar:
                while True:
                    
                    any_tick_processed = False

                    for ticks_info in self.TESTER_ALL_TICKS_INFO:

                        symbol = ticks_info["symbol"]
                        size = ticks_info["size"]
                        counter = ticks_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = ticks_info["ticks"].row(counter)

                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        ticks_info["counter"] = counter + 1
                        any_tick_processed = True

                        pbar.update(1)

                    if not any_tick_processed:
                        break

Das folgende Ergebnis wurde erzielt, als eine Klasse Tester mit der Einstellung des Models auf every_tick in einer JSON-Konfigurationsdatei ausgeführt wurde.

Tester Progress: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 20613922/20613922 [01:31<00:00, 224889.74tick/s]

Nicht schlecht, es dauerte anderthalb Minuten, um das gesamte Jahr mit drei Instrumenten zu simulieren.


Strategieprüfung auf der Grundlage von NEUER BAR

Dies ist der schnellste und am wenigsten genaue Modellierungsmodus im Strategietester des MetaTrader 5. Dabei wird das Programm nur bei der Eröffnung einer neuen Bar getestet. Er überspringt alle Ticks zwischen dem Öffnen und Schließen eines Balkens.

Innerhalb des Klassenkonstruktors folgen wir denselben Grundsätzen wie bei der Vorbereitung echter Ticks für die Simulation.

Dieses Mal speichern wir die von jedem Instrument gesammelten Balken in einem Array namens TESTER_ALL_BARS_INFO.

tester.py

        self.TESTER_ALL_BARS_INFO = [] # for storing all bars to be used during the test
        
        for symbol in self.tester_config["symbols"]:
            
            if modelling == "real_ticks":
                    
                # ....
            
            elif modelling == "new_bar":
                
                bars_obtained = bars.fetch_historical_bars(symbol=symbol, 
                                                        timeframe=utils.TIMEFRAMES[self.tester_config["timeframe"]],
                                                        start_datetime=start_dt,
                                                        end_datetime=end_dt)
                
                bars_info = {
                    "symbol": symbol,
                    "bars": bars_obtained,
                    "size": bars_obtained.height,
                    "counter": 0
                }
                
                self.TESTER_ALL_BARS_INFO.append(bars_info)

Innerhalb der OnTick-Funktion durchlaufen wir eine Schleife durch alle gesammelten Balken.

    def OnTick(self, ontick_func):
        
        #....
                    
        elif modelling == "new_bar":
            
            bars_ = [bars_info["size"] for bars_info in self.TESTER_ALL_BARS_INFO]
            total_bars = sum(bars_)
            
            self.__GetLogger().debug(f"total number of bars: {total_bars}")

            with tqdm(total=total_bars, desc="Tester Progress", unit="bar") as pbar:
                while True:
                    
                    self.__account_monitoring()
                    self.__positions_monitoring()
                    self.__pending_orders_monitoring()
                    
                    any_bar_processed = False

                    for bars_info in self.TESTER_ALL_BARS_INFO:

                        symbol = bars_info["symbol"]
                        size = bars_info["size"]
                        counter = bars_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = self._bar_to_tick(symbol=symbol, bar=bars_info["bars"].row(counter))
                        
                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        bars_info["counter"] = counter + 1
                        any_bar_processed = True

                        pbar.update(1)

                    if not any_bar_processed:
                        break

Unsere Tester-Klasse ist jedoch stark auf Ticks angewiesen. Da ein Balken nicht mit einem Tick gleichzusetzen ist, erzeugen wir Ticks bei der Eröffnung eines Balkens mithilfe der folgenden Funktion:

    def _bar_to_tick(self, symbol, bar):
        """
            Creates a synthetic tick from a bar (MT5-style).
            Uses OPEN price.
        """

        price = bar["open"] if isinstance(bar, dict) else bar[1]
        time = bar["time"] if isinstance(bar, dict) else bar[0]
        spread = bar["spread"] if isinstance(bar, dict) else bar[6]
        tv = bar["tick_volume"] if isinstance(bar, dict) else bar[5]
        
        return {
            "time": time,
            "bid": price,
            "ask": price + spread * self.symbol_info(symbol).point,
            "last": price,
            "volume": tv,
            "time_msc": time.timestamp(),
            "flags": 0,
            "volume_real": 0,
        }

Wir behandeln den aktuellen Eröffnungskurs als Geldkurs.

Wir gehen davon aus, dass der Briefkurs die Summe aus dem aktuellen Eröffnungskurs und dem Spread eines bestimmten Balkens in Punkten ist.

Nachfolgend ist das Ergebnis zu sehen, wenn eine Klasse mit dem Modellierungswert new_bar ausgeführt wird.

Tester Progress: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████| 18579/18579 [00:00<00:00, 158324.66bar/s]

Der Test erfolgte sofort, was wir erwarten, wenn die Modellierung new_bar angewendet wird.


Strategieprüfung auf 1-Minuten-OHLC

Dieser Modellierungstyp zielt darauf ab, beim Testen von Programmen in MetaTrader 5 das richtige Gleichgewicht zwischen der Genauigkeit und der Geschwindigkeit des Strategietesters herzustellen.

In diesem Modus verwendet das Terminal die Balken des 1-Minuten-Charts, um die Ticks bei der Eröffnung eines Balkens zu erzeugen, ähnlich wie wir die Ticks für den neuen Balkenmodus erzeugt haben.

Wir holen die Balken auf die gleiche Weise wie bei der vorherigen Modellierungsmethode; der einzige Unterschied ist ein Zeitrahmenargument.

elif modelling == "1-minute-ohlc":
                
    bars_obtained = bars.fetch_historical_bars(symbol=symbol, 
                                            timeframe=utils.TIMEFRAMES["M1"],
                                            start_datetime=start_dt,
                                            end_datetime=end_dt)
                
    bars_info = {
        "symbol": symbol,
        "bars": bars_obtained,
        "size": bars_obtained.height,
        "counter": 0
    }
                
    self.TESTER_ALL_BARS_INFO.append(bars_info)

Auch hier sind die beiden Modellierungsmodi ähnlich, sodass wir die gleiche Schleife wie bei der Modellierungsmethode new_bar verwenden.

        elif modelling == "new_bar" or modelling == "1-minute-ohlc":
            
            bars_ = [bars_info["size"] for bars_info in self.TESTER_ALL_BARS_INFO]
            total_bars = sum(bars_)
            
            self.__GetLogger().debug(f"total number of bars: {total_bars}")

            with tqdm(total=total_bars, desc="Tester Progress", unit="bar") as pbar:
                while True:
                    
                    any_bar_processed = False

                    for bars_info in self.TESTER_ALL_BARS_INFO:

                        symbol = bars_info["symbol"]
                        size = bars_info["size"]
                        counter = bars_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = self._bar_to_tick(symbol=symbol, bar=bars_info["bars"].row(counter))
                        
                        # Getting ticks at the current bar
                        
                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        bars_info["counter"] = counter + 1
                        any_bar_processed = True

                        pbar.update(1)

                    if not any_bar_processed:
                        break

configs/tester.json:

{
    "tester": {
        "bot_name": "MY EA",
        "symbols": ["EURUSD", "USDCAD", "USDJPY"],
        "timeframe": "H1",
        "start_date": "01.01.2025 00:00",
        "end_date": "31.12.2025 00:00",
        "modelling" : "1-minute-ohlc",
        "deposit": 1000,
        "leverage": "1:100"
    }
}

Nachfolgend das Ergebnis der Durchführung des Kurses:

2026-01-11 17:59:45,462 | DEBUG    | MY EA.tester | [tester.py:1940 -     OnTick() ] => total number of bars: 1113189
Tester Progress: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████| 1113189/1113189 [00:07<00:00, 143610.20bar/s]

Sie ist die zweitschnellste nach der new_bar-Modellierungsmethode. 


Alles in der OnTick-Funktion vereinen

In früheren Artikeln dieser Serie haben wir verschiedene Funktionen zur Überwachung des Kontos, der ausstehenden Aufträge und der Positionen implementiert. Da wir keine Möglichkeit hatten, sie zu testen, werden wir sie dieses Mal in der OnTick-Methode unserer Klasse in Aktion setzen.

    def OnTick(self, ontick_func):
        """Calls the assigned function upon the receival of new tick(s)

        Args:
            ontick_func (_type_): A function to be called on every tick
        """
        
        if not self.IS_TESTER:
            return

        modelling = self.tester_config["modelling"]
        if modelling == "real_ticks" or modelling == "every_tick":
            
            total_ticks = sum(ticks_info["size"] for ticks_info in self.TESTER_ALL_TICKS_INFO)

            self.__GetLogger().debug(f"total number of ticks: {total_ticks}")

            with tqdm(total=total_ticks, desc="Tester Progress", unit="tick") as pbar:
                while True:
                    
                    self.__account_monitoring()
                    self.__positions_monitoring()
                    self.__pending_orders_monitoring()
                    
                    any_tick_processed = False

                    for ticks_info in self.TESTER_ALL_TICKS_INFO:

                        symbol = ticks_info["symbol"]
                        size = ticks_info["size"]
                        counter = ticks_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = ticks_info["ticks"].row(counter)

                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        ticks_info["counter"] = counter + 1
                        any_tick_processed = True

                        pbar.update(1)

                    if not any_tick_processed:
                        break
                    
        elif modelling == "new_bar" or modelling == "1-minute-ohlc":
            
            bars_ = [bars_info["size"] for bars_info in self.TESTER_ALL_BARS_INFO]
            total_bars = sum(bars_)
            
            self.__GetLogger().debug(f"total number of bars: {total_bars}")

            with tqdm(total=total_bars, desc="Tester Progress", unit="bar") as pbar:
                while True:
                    
                    self.__account_monitoring()
                    self.__positions_monitoring()
                    self.__pending_orders_monitoring()
                    
                    any_bar_processed = False

                    for bars_info in self.TESTER_ALL_BARS_INFO:

                        symbol = bars_info["symbol"]
                        size = bars_info["size"]
                        counter = bars_info["counter"]

                        if counter >= size:
                            continue

                        current_tick = self._bar_to_tick(symbol=symbol, bar=bars_info["bars"].row(counter))
                        
                        # Getting ticks at the current bar
                        
                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        bars_info["counter"] = counter + 1
                        any_bar_processed = True

                        pbar.update(1)

                    if not any_bar_processed:
                        break

Bei neuen Balkenmodi (1-Minuten-Balken und neuer Balken) rufen wir diese Funktionen bei der Eröffnung jedes Balkens auf, während wir sie bei tickbasierten Modi bei jedem Tick aufrufen.


Zum Schluss noch einige Handelsaktionen im Strategietester

Lassen Sie uns nun unseren allerersten Handelsroboter mit diesem Simulator erstellen und beobachten, wie er funktioniert.

Als Erstes initialisieren wir das gewünschte MetaTrader 5-Terminal, nachdem wir sein Modul zusammen mit anderen nützlichen Python-Modulen für dieses Projekt importiert haben.

example_bot.py

import MetaTrader5 as mt5
from tester import Tester
from Trade.Trade import CTrade
import json
import os
import config

if not mt5.initialize(): # Initialize MetaTrader5 instance
    print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}")
    mt5.shutdown()
    quit()

Anschließend laden wir die Testerkonfigurationen aus einer Datei configs/tester.json.

try:
    with open(os.path.join(config.CONFIGS_DIR,'tester.json'), 'r', encoding='utf-8') as file: # reading a JSON file
        # Deserialize the file data into a Python object
        tester_configs = json.load(file)
except Exception as e:
    raise RuntimeError(e)

  Wir initialisieren die Tester-Klasse, indem wir ihr Konfigurationen und eine initialisierte MetaTrader 5-Instanz geben.

tester = Tester(tester_config=tester_configs["tester"], mt5_instance=mt5) # very important

Wir benötigen einige globale Variablen, die als Eingaben fungieren, die wir oft in MetaTrader 5-Programmen sehen.

symbol = "EURUSD"
timeframe = "PERIOD_H1"
magic_number = 10012026
slippage = 100
sl = 500
tp = 700

Optional können wir die Klasse CTrade instanziieren, um uns das Leben zu erleichtern.

m_trade = CTrade(simulator=tester, magic_number=magic_number, filling_type_symbol=symbol, deviation_points=slippage)

Wir bernötigen auch Informationen über ein bestimmtes Symbol, das uns zur Verfügung steht.

symbol_info = tester.symbol_info(symbol=symbol)

Jeder Handelsroboter benötigt eine Strategie. Lassen Sie uns eine schreiben:

Wenn es keine offenen Positionen dieser Art gibt, eröffnen wir eine und halten sie, bis sie entweder durch einen Stop-Loss- oder Take-Profit-Treffer geschlossen wird, und der Prozess wiederholt sich.

Wir bernötigen also eine Funktion, die prüft, ob eine Position mit bestimmten Attributen (Typ und magische Zahl) existiert.

def pos_exists(magic: int, type: int) -> bool:

    for position in tester.positions_get():
        if position.type == type and position.magic == magic:
            return True
    
    return False

  Wir schaffen eine Hauptfunktion für die Umsetzung unserer schönen Strategie.

def on_tick():
    
    tick_info = tester.symbol_info_tick(symbol=symbol)
    
    ask = tick_info.ask
    bid = tick_info.bid
    
    pts = symbol_info.point
    
    if not pos_exists(magic=magic_number, type=mt5.POSITION_TYPE_BUY): # If a position of such kind doesn't exist
        m_trade.buy(volume=0.1, symbol=symbol, price=ask, sl=ask-sl*pts, tp=ask+tp*pts, comment="Tester buy") # we open a buy position
    
    if not pos_exists(magic=magic_number, type=mt5.POSITION_TYPE_SELL): # If a position of such kind doesn't exist
        m_trade.sell(volume=0.1, symbol=symbol, price=bid, sl=bid+sl*pts, tp=bid-tp*pts, comment="Tester sell") # we open a sell position

Am Ende unseres Programms übergeben wir diese Funktion an die Methode OnTick der Klasse Tester.

tester.OnTick(ontick_func=on_tick) # very important!

Schließlich führen wir eine Strategietest-Aktion durch.

Bei einer sorgfältigen Prüfung konnte ich einige Handelsvorgänge (Öffnen und Schließen von Positionen) feststellen.

2026-01-11 20:03:42,943 | INFO     | MY EA.tester | [tester.py:1335 - order_send() ] => Position: 113161665468402118449 opened!
Tester Progress:  79%|██████████████████▏    | 882118/1113189 [00:19<00:05, 46032.14bar/s]2026-01-11 20:03:43,349 | INFO     | MY EA.tester | [tester.py:1251 - order_send() ] => Position: 113161665468402118449 closed!
2026-01-11 20:03:43,351 | INFO     | MY EA.tester | [tester.py:1335 - order_send() ] => Position: 113161665494484307258 opened!
2026-01-11 20:03:43,353 | INFO     | MY EA.tester | [tester.py:1251 - order_send() ] => Position: 113161665462461689618 closed!
2026-01-11 20:03:43,353 | INFO     | MY EA.tester | [tester.py:1335 - order_send() ] => Position: 113161665494609542402 opened!
Tester Progress:  80%|██████████████████▎    | 886723/1113189 [00:19<00:04, 45496.53bar/s]2026-01-11 20:03:43,452 | INFO     | MY EA.tester | [tester.py:1251 - order_send() ] => Position: 113161665494609542402 closed!
2026-01-11 20:03:43,453 | INFO     | MY EA.tester | [tester.py:1335 - order_send() ] => Position: 113161665501001632037 opened!
2026-01-11 20:03:43,473 | INFO     | MY EA.tester | [tester.py:1251 - order_send() ] => Position: 113161665494484307258 closed!
2026-01-11 20:03:43,474 | INFO     | MY EA.tester | [tester.py:1335 - order_send() ] => Position: 113161665502337760048 opened!
Tester Progress:  80%|██████████████████▍    | 891275/1113189 [00:19<00:04, 45088.52bar/s]2026-01-11 20:03:43,501 | INFO     | MY EA.tester | [tester.py:1251 - order_send() ] => Position: 113161665502337760048 closed!

Großartig, auf den ersten Blick scheint alles zu funktionieren. Lassen Sie uns dies näher analysieren.


Erzeugen von Strategietester-Berichten

Nach einem erfolgreichen Strategietesterlauf erstellt das MetaTrader 5-Terminal einen sogenannten Strategietesterbericht. Dies ist ein Bericht, der statistische Metriken für alle Vorgänge enthält, die während einer Strategietestaktion ausgelöst wurden.

Zu diesen Kennzahlen gehören der Gesamtnettogewinn, der Bruttogewinn bzw. -verlust sowie die Gewinnquote des Programms sowohl bei Short- als auch bei Long-Trades usw. 

Neben einem Backtest-Bericht, der im MetaTrader 5-Terminal zu finden ist, sind wir an einem HTML-Bericht interessiert, der aus diesem Bericht extrahiert werden kann.

Lassen Sie uns einen solchen Bericht auch in unserem nutzerdefinierten Strategietester erstellen, indem wir mit einer einfachen Berichtsvorlage beginnen.

Berichte/vorlage.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Strategy Tester</title>

    <!-- Bootstrap 5 -->
    <link
        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
        rel="stylesheet"
    >
    
    <style>
        body {
            background: #f8f9fa;
        }

        h4 {
            font-size: 14px;
            text-align: center;
            margin: 16px 0 10px;
            font-weight: 600;
        }

        .tester-container {
            max-width: 1200px;
            margin: auto;
        }

        .table-wrapper {
            margin: 0 10%; /* 10% space left and right */
        }

        .table {
            font-size: 10px;
            width: 100%; /* fill the wrapper */
            background: white;
        }

        .table th {
            white-space: nowrap;
            text-align: center;
            font-size: 10px;
        }

        .table td {
            white-space: nowrap;
            font-size: 10px;
        }
    </style>

</head>
<body>

<h4 class="mt-4 text-center">Orders</h4>
<div class="table-wrapper">
    <div class="table-responsive">
        <table class="table table-sm table-striped table-bordered align-middle">
            <thead class="table-light text-center">
                <tr>
                    <th>Open Time</th>
                    <th>Order</th>
                    <th>Symbol</th>
                    <th>Type</th>
                    <th class="text-end">Volume</th>
                    <th class="text-end">Price</th>
                    <th class="text-end">S / L</th>
                    <th class="text-end">T / P</th>
                    <th>Time</th>
                    <th>State</th>
                    <th>Comment</th>
                </tr>
            </thead>
            <tbody>
                {{ORDER_ROWS}}
            </tbody>
        </table>
    </div>
</div>

<h4 class="mb-3 text-center">Deals</h4>
<div class="table-wrapper">
    <div class="table-responsive">
        <table class="table table-sm table-striped table-bordered align-middle">
            <thead class="table-light text-center">
                <tr>
                    <th>Time</th>
                    <th>Deal</th>
                    <th>Symbol</th>
                    <th>Type</th>
                    <th>Entry</th>
                    <th>Volume</th>
                    <th>Price</th>
                    <th>Commission</th>
                    <th>Swap</th>
                    <th>Profit</th>
                    <th>Comment</th>
                    <th>Balance</th>
                </tr>
            </thead>
            <tbody>
                {{DEAL_ROWS}}
            </tbody>
        </table>
    </div>
</div>

</body>
</html>

I: Schreiben der Auftragshistorie in den Bericht 

Wir beginnen mit dem einfachsten Teil dieses Berichts – der Anzeige aller in einer Simulation erteilten oder ausgelösten Aufträge.

Um diese Aufträge iterativ anzuzeigen, wird ein Array innerhalb der Klasse namens __orders_history_container__ verwendet.

Dieses Array wird jedes Mal aufgefüllt, wenn ein Auftrag erteilt oder eine Position in der Methode order_send geöffnet/geschlossen wird.

tester.py

    def __GenerateTesterReport(self, output_file="Tester report.html"):
        
        def render_order_rows(orders):
            rows = []

            for o in orders:
                rows.append(f"""
                <tr>
                    <td>{datetime.fromtimestamp(o.time_setup)}</td>
                    <td>{o.ticket}</td>
                    <td>{o.symbol}</td>
                    <td>{utils.ORDER_TYPE_MAP.get(o.type, o.type)}</td>
                    <td class="text-end">{o.volume_initial:.2f} / {o.volume_current:.2f}</td>
                    <td class="text-end">{o.price_open:.5f}</td>
                    <td class="text-end">{"" if o.sl == 0 else f"{o.sl:.5f}"}</td>
                    <td class="text-end">{"" if o.tp == 0 else f"{o.tp:.5f}"}</td>
                    <td>{datetime.fromtimestamp(o.time_done) if o.time_done else ""}</td>
                    <td>{utils.ORDER_STATE_MAP.get(o.state, o.state)}</td>
                    <td>{o.comment}</td>
                </tr>
                """)

            return "\n".join(rows)


        with open("Reports/template.html", "r", encoding="utf-8") as f:
            template = f.read()

        order_rows_html = render_order_rows(self.__orders_history_container__)
        
        # we populate table's body

        html = (
            template
            .replace("{{ORDER_ROWS}}", order_rows_html)
        )

        with open(output_file, "w", encoding="utf-8") as f:
            f.write(html)

        print(f"Deals report saved to: {output_file}")

Ausgabe:

II: Schreiben der Deals in den Bericht

Da wir in unserer Klasse ein Array namens __deals_history_container__ haben, das alle während der Simulation geöffneten Deals speichert, extrahieren wir einige Informationen daraus, ähnlich wie wir es bei den Aufträgen gemacht haben, und fügen sie in eine Vorlage für den Bericht ein.

tester.py

    def __GenerateTesterReport(self, output_file="Tester report.html"):
        
        def render_order_rows(orders):
            rows = []

            for o in orders:
                rows.append(f"""
                <tr>
                    <td>{datetime.fromtimestamp(o.time_setup)}</td>
                    <td>{o.ticket}</td>
                    <td>{o.symbol}</td>
                    <td>{utils.ORDER_TYPE_MAP.get(o.type, o.type)}</td>
                    <td class="text-end">{o.volume_initial:.2f} / {o.volume_current:.2f}</td>
                    <td class="text-end">{o.price_open:.5f}</td>
                    <td class="text-end">{"" if o.sl == 0 else f"{o.sl:.5f}"}</td>
                    <td class="text-end">{"" if o.tp == 0 else f"{o.tp:.5f}"}</td>
                    <td>{datetime.fromtimestamp(o.time_done) if o.time_done else ""}</td>
                    <td>{utils.ORDER_STATE_MAP.get(o.state, o.state)}</td>
                    <td>{o.comment}</td>
                </tr>
                """)

            return "\n".join(rows)

        def render_deal_rows(deals):
            rows = []

            for d in deals:

                rows.append(f"""
                <tr>
                    <td>{datetime.fromtimestamp(d.time)}</td>
                    <td>{d.ticket}</td>
                    <td>{d.symbol}</td>
                    <td>{utils.DEAL_TYPE_MAP[d.type]}</td>
                    <td>{utils.DEAL_ENTRY_MAP[d.entry]}</td>
                    <td class="text-end">{d.volume:.2f}</td>
                    <td class="text-end">{d.price:.5f}</td>
                    <td class="text-end">{d.commission:.2f}</td>
                    <td class="text-end">{d.swap:.2f}</td>
                    <td class="text-end">{d.profit:.2f}</td>
                    <td>{d.comment}</td>
                    <td>{round(d.balance, 2)}</td>
                </tr>
                """)

            return "\n".join(rows)


        with open("Reports/template.html", "r", encoding="utf-8") as f:
            template = f.read()

        order_rows_html = render_order_rows(self.__orders_history_container__)
        deal_rows_html = render_deal_rows(self.__deals_history_container__)
        
        # we populate table's body
        
        html = (
            template
            .replace("{{ORDER_ROWS}}", order_rows_html)
            .replace("{{DEAL_ROWS}}", deal_rows_html)
        )

        with open(output_file, "w", encoding="utf-8") as f:
            f.write(html)

        print(f"Deals report saved to: {output_file}")

Wenn Sie nun darauf achten, wie Deals in einem MetaTrader 5-Bericht geschrieben werden, werden Sie feststellen, dass der erste Deal ein Saldo-Deal ist (der die Ersteinlage betrifft). Im Folgenden wird beschrieben, wie wir das gleiche Ergebnis erreichen können.

    def __make_balance_deal(self, time: datetime) -> namedtuple:

        time_sec = int(time.timestamp())
        time_msc = int(time.timestamp() * 1000)

        return self.TradeDeal(
            ticket=self.__generate_deal_ticket(),
            order=0,
            time=time_sec,
            time_msc=time_msc,
            type=self.mt5_instance.DEAL_TYPE_BALANCE,
            entry=self.mt5_instance.DEAL_ENTRY_IN,
            magic=0,
            position_id=0,
            reason=np.nan,
            volume=np.nan,
            price=np.nan,
            commission=0.0,
            swap=0.0,
            profit=0.0,
            fee=0.0,
            symbol="",
            balance=self.AccountInfo.balance,
            comment="",
            external_id=""
        )
Wir erstellen einen Deal mit einem Saldo-Typ und fügen ihn zu Beginn einer Simulation an das Container-Array für die Dealshistorie an, sodass er der erste Deal in der Historie ist.
    def __TesterInit(self):
        
        self.__deals_history_container__.append(
            self.__make_balance_deal(time=self.tester_config["start_date"])
        )
        
    def __TesterDeinit(self):
        
        # generate a report at the end
        self.__GenerateTesterReport(output_file=f"Reports/{self.tester_config['bot_name']}-report.html")

Beachten Sie, dass Sie die Funktion zur Erstellung eines Testerberichts innerhalb der Funktion __TesterDeinit aufrufen. Diese Funktion soll am Ende unserer Tester-Simulation aufgerufen werden; alle mathematischen Berechnungen, die Analyse, die Erstellung und das Speichern eines Berichts werden innerhalb dieser Methode durchgeführt.

 Ausgabe:

III: Schreiben der Statistiken des Testers

Um die gleichen Metriken zu imitieren, die das MetaTrader 5-Terminal bietet, müssen wir verstehen, was die einzelnen Metriken darstellen und sie dann manuell für unseren Bericht ableiten.

Metrik Beschreibung Berechnung (MT5)
History Quality Qualität der verwendeten historischen Daten (modellierte Ticks ÷ erforderliche Ticks) × 100%
Bars Anzahl der verarbeiteten Stäbe Gesamte Balken im Testzeitraum
Ticks Anzahl der verwendeten Preis-Ticks Verarbeitete Tick-Ereignisse insgesamt
Symbols An der Prüfung beteiligte Symbole Anzahl der gehandelten Symbole
Total Net Profit Endgültiges Handelsergebnis Bruttogewinn + Bruttoverlust
Gross Profit Gesamtgewinn aus erfolgreichen Trades Σ (Gewinn > 0)
Gross Loss Gesamtverlust aus Verlustgeschäften Σ (Verlust < 0)
Profit Factor Gewinnrate Bruttogewinn ÷ |Bruttoverlust|
Expected Payoff Durchschnittlicher Gewinn pro Trade Nettogewinn ÷ Gesamtumsatz
Recovery Factor Fähigkeit, sich von einer Inanspruchnahme zu erholen Nettogewinn ÷ Maximaler Drawdown
Sharpe Ratio Risikobereinigte Rendite Mittelwert(Erträge) ÷ Std(Erträge)
Z-Score Zufälligkeit der Handelsreihenfolge Statistischer Z-Test zu Gewinn/Verlust-Läufen
AHPR Arithmetische Haltezeitrendite
GHPR Geometrische Haltedauerrendite
GHPR=(BalanceClose/BalanceOpen)^(1/N)
LR Correlation Stärke des Kapitaltrends Korrelation (Handelsindex, Kapital)
LR Standard Error Abweichung vom Kapitaltrend Std. Fehler der Regressionsresiduen
Margin Level Sicherheitsniveau des Kontos (Kapital ÷ Marge) × 100%
Total Trades Anzahl geschlossener Positionen Anzahl der geschlossenen Positionen
Total Deals Alle Deal-Daten Einschließlich partieller Schließungen
Short-Trades (in %) Gewinnrate der Short-Trades Shorts-Trade mit Gewinn ÷ alle Shorts-Trades × 100
Long-Trades (Gewinner %) Gewinnrate bei Long-Trades Long-Trades mit Gewinn ÷ gesamte Long-Trades × 100
Profit-Trades (%) Gewinnrate des Handels Abschlüsse mit Gewinn ÷ alle Abschlüsse × 100
Loss-Trades (%) Verlustrate im Handel Verlierer ÷ alle Trades × 100
Largest Profit Trade Bester Einzel-Trade max(Handelsgewinn)
Größter Verlust-Trade Schlechtester Einzel-Trade min(Handelsgewinn)
Average Profit Trade Mittlerer Gewinn der Gewinner Σ Gewinn ÷ Anzahl der erfolgreichen Abschlüsse
Average Loss Trade Mittlerer Verlust der Verlierer Σ Verluste ÷ Anzahl der Verlustgeschäfte
Maximum Consecutive Wins Längste Gewinnserie max(Anzahl der aufeinanderfolgenden Gewinne)
Maximum Consecutive Losses Längste Verlustserie max(Anzahl der aufeinanderfolgenden Verluste)
Maximal Consecutive Profit Größter Wert einer Gewinnserie max(Σ Gewinn in Gewinnserie)
Maximal Consecutive Loss Größter Wert einer Verlustserie min(Σ Verlust in Verlustserie)
Average Consecutive Wins Durchschnittliche Länge der Gewinnserie Mittelwert (Länge der Gewinnserie)
Average Consecutive Losses Durchschnittliche Länge der Verlustserie Mittelwert (Verlustserie)
Balance Drawdown Absolute Saldorückgang seit Beginn Anfangssaldo – Mindestsaldo
Equity Drawdown Absolute Kapitalrückgang seit Beginn Anfangskapital – Mindestkapital
Balance Drawdown Maximal Größter Rückgang vom Maximum zum Minimum des Saldos max(Saldospitze – Tiefpunkt)
Equity Drawdown Maximal Größter Rückgang vom Maximum zum Minimum des Kapitals max(Höchststand des Kapitals – Tiefststand)
Balance Drawdown Relative Maximaler Drawdown des Saldos % (Max DD ÷ Höchstwert des Saldo) × 100
Equity Drawdown Relative Maximaler Drawdown des Kapitals in % (Max DD ÷ Höchstwert des Kapitals) × 100

Vorerst werden wir einige der am häufigsten verwendeten Metriken in unseren Bericht aufnehmen.

tester.py

    def __TesterDeinit(self):
        
        profits = []
        losses = []
        total_trades = 0
        
        max_consec_win_count = 0
        max_consec_win_money = 0.0

        max_consec_loss_count = 0
        max_consec_loss_money = 0.0

        max_profit_streak_money = 0.0
        max_profit_streak_count = 0

        max_loss_streak_money = 0.0
        max_loss_streak_count = 0

        cur_win_count = 0
        cur_win_money = 0.0

        cur_loss_count = 0
        cur_loss_money = 0.0

        win_streaks = []
        loss_streaks = []
        
        short_trades_won = 0
        long_trades_won = 0
        
        for deal in self.__deals_history_container__:
            if deal.entry == self.mt5_instance.DEAL_ENTRY_OUT: # a closed position
                
                total_trades +=1
                
                profit = deal.profit
                    
                if profit > 0: # A win
                    
                    profits.append(profit)
                    
                    # reset loss streak
                    if cur_loss_count > 0:
                        loss_streaks.append(cur_loss_count)
                        cur_loss_count = 0
                        cur_loss_money = 0.0

                    cur_win_count += 1
                    cur_win_money += profit

                    # longest win streak
                    if cur_win_count > max_consec_win_count:
                        max_consec_win_count = cur_win_count
                        max_consec_win_money = cur_win_money

                    # most profitable win streak
                    if cur_win_money > max_profit_streak_money:
                        max_profit_streak_money = cur_win_money
                        max_profit_streak_count = cur_win_count

                    if deal.type == self.mt5_instance.DEAL_TYPE_BUY:
                        long_trades_won += 1
                    
                    if deal.type == self.mt5_instance.DEAL_TYPE_SELL:
                        short_trades_won += 1
                        
                else: # A loss
                    
                    losses.append(profit)
                    
                    # reset win streak
                    if cur_win_count > 0:
                        win_streaks.append(cur_win_count)
                        cur_win_count = 0
                        cur_win_money = 0.0

                    cur_loss_count += 1
                    cur_loss_money += profit

                    # longest loss streak
                    if cur_loss_count > max_consec_loss_count:
                        max_consec_loss_count = cur_loss_count
                        max_consec_loss_money = cur_loss_money

                    # largest losing streak
                    if cur_loss_money < max_loss_streak_money:
                        max_loss_streak_money = cur_loss_money
                        max_loss_streak_count = cur_loss_count
                    
        
        self.tester_stats["Gross Profit"] = np.sum(profits) if profits else 0
        self.tester_stats["Gross Loss"] = np.sum(losses) if losses else 0
        self.tester_stats["Net Profit"] = self.tester_stats["Gross Profit"] + self.tester_stats["Gross Loss"]
        
        self.tester_stats["Profit Factor"] = self.tester_stats["Gross Profit"] / self.tester_stats["Gross Loss"]
        
        self.tester_stats["Expected Payoff"] = (
            self.tester_stats["Net Profit"] / total_trades
            if total_trades > 0 else 0
        )

        def max_drawdown(curve):
            peak = curve[0]
            max_dd = 0.0

            for value in curve:
                peak = max(peak, value)
                dd = peak - value
                max_dd = max(max_dd, dd)

            return max_dd

        returns = np.diff(self.tester_curves["equity"])

        sharpe = (
            np.mean(returns) / np.std(returns)
            if len(returns) > 1 and np.std(returns) > 0 else 0.0
        )
        
        self.tester_stats["Sharpe Ratio"] = sharpe
        
        self.tester_stats["Equity Drawdown Absolute"] = max_drawdown(self.tester_curves["equity"])
        self.tester_stats["Balance Drawdown Absolute"] = max_drawdown(self.tester_curves["balance"])
        
        self.tester_stats["Recovery Factor"] = (
            self.tester_stats["Net Profit"] / max(self.tester_stats["Balance Drawdown Absolute"], 1)
        )

        self.tester_stats["Equity Drawdown Relative"] = (
            self.tester_stats["Equity Drawdown Absolute"] / max(self.tester_curves["equity"]) * 100
            if self.tester_curves["equity"] else 0.0
        )
        
        self.tester_stats["Balance Drawdown Relative"] = (
            self.tester_stats["Balance Drawdown Absolute"] / max(self.tester_curves["balance"]) * 100
            if self.tester_curves["balance"] else 0.0
        )
        
        self.tester_stats["Balance Drawdown Maximal"] = max_drawdown(self.tester_curves["balance"])
        self.tester_stats["Equity Drawdown Maximal"] = max_drawdown(self.tester_curves["equity"])
        
        self.tester_stats["Total Trades"] = total_trades
        self.tester_stats["Total Deals"] = len(self.__deals_history_container__)
        
        self.tester_stats["Short Trades Won"] = short_trades_won
        self.tester_stats["Long Trades Won"] = long_trades_won
        
        self.tester_stats["Profit Trades"] = len(profits) if profits else 0
        self.tester_stats["Loss Trades"] = len(losses) if losses else 0
        
        self.tester_stats["Largest Profit Trade"] = np.max(profits) if profits else 0
        self.tester_stats["Largest Loss Trade"] = np.min(losses) if losses else 0
        
        self.tester_stats["Average Profit Trade"] = np.mean(profits) if profits else 0
        self.tester_stats["Average Loss Trade"] = np.mean(losses) if losses else 0
        
        self.tester_stats["Maximum Consecutive Wins"] = max_profit_streak_count
        self.tester_stats["Maximum Consecutive Losses"] = max_loss_streak_count
        
        self.tester_stats["Maximum Consecutive Wins Money"] = max_profit_streak_money
        self.tester_stats["Maximum Consecutive Losses Money"] = max_loss_streak_money
        
        self.tester_stats["Average Consecutive Wins"] = np.mean(win_streaks)
        self.tester_stats["Average Consecutive Losses"] = np.mean(loss_streaks)
        
        # AHPR / GHPR
        
        self.tester_stats["AHPR"] = np.prod(1 + returns) ** (1/len(returns)) if len(returns) else 0
        self.tester_stats["GHPR"] = np.prod(1 + returns) if len(returns) else 0

In der Funktion __GenerateTesterReport übertragen wir diese Kennziffern in unsere HTML-Vorlage, ähnlich wie bei der Übertragung von Aufträgen und Deals.

        stats_table = f"""
            <table class="report-table table-sm table-striped">
                <tbody>
                    <tr>
                        <th>Initial Deposit</th><td class="number">{self.tester_config.get('deposit', 0)}</td>
                        <th>Ticks</th><td class="number">{self.tester_stats.get('Ticks', 0)}</td>
                        <th>Symbols</th><td class="number">{self.tester_stats.get('Symbols', 0)}</td>
                    </tr>
                    <tr>
                        <th>Total Net Profit</th><td class="number">{self.tester_stats.get('Net Profit', 0):.2f}</td>
                        <th>Balance Drawdown Absolute</th><td class="number">{self.tester_stats.get('Balance Drawdown Absolute', 0):.2f}</td>
                        <th>Equity Drawdown Absolute</th><td class="number">{self.tester_stats.get('Equity Drawdown Absolute', 0):.2f}</td>
                    </tr>
                    <tr>
                        <th>Gross Profit</th><td class="number">{self.tester_stats.get('Gross Profit', 0):.2f}</td>
                        <th>Balance Drawdown Maximal</th><td class="number">{self.tester_stats.get('Balance Drawdown Maximal', 0):.2f}</td>
                        <th>Equity Drawdown Maximal</th><td class="number">{self.tester_stats.get('Equity Drawdown Maximal', 0):.2f}</td>
                    </tr>
                    <tr>
                        <th>Gross Loss</th><td class="number">{self.tester_stats.get('Gross Loss', 0):.2f}</td>
                        <th>Balance Drawdown Relative</th><td class="number">{self.tester_stats.get('Balance Drawdown Relative', 0):.2f}%</td>
                        <th>Equity Drawdown Relative</th><td class="number">{self.tester_stats.get('Equity Drawdown Relative', 0):.2f}%</td>
                    </tr>
                    <tr>
                        <th>Profit Factor</th><td class="number">{self.tester_stats.get('Profit Factor', 0):.2f}</td>
                        <th>Expected Payoff</th><td class="number">{self.tester_stats.get('Expected Payoff', 0):.2f}</td>
                        <th>Margin Level</th><td class="number">{self.tester_stats.get('Margin Level', 0):.2f}%</td>
                    </tr>
                    <tr>
                        <th>Recovery Factor</th><td class="number">{self.tester_stats.get('Recovery Factor', 0):.2f}</td>
                        <th>Sharpe Ratio</th><td class="number">{self.tester_stats.get('Sharpe Ratio', 0):.2f}</td>
                        <th>Z-Score</th><td class="number">{self.tester_stats.get('Z-Score', 0):.2f}</td>
                    </tr>
                    <tr>
                        <th>AHPR</th><td class="number">{self.tester_stats.get('AHPR', 0):.4f}</td>
                        <th>LR Correlation</th><td class="number">{self.tester_stats.get('LR Correlation', 0):.2f}</td>
                        <th>OnTester result</th><td class="number">{self.tester_stats.get('OnTester result', 0)}</td>
                    </tr>
                    <tr>
                        <th>GHPR</th><td class="number">{self.tester_stats.get('GHPR', 0):.4f}</td>
                        <th>LR Standard Error</th><td class="number">{self.tester_stats.get('LR Standard Error', 0):.2f}</td>
                        <td></td><td></td>
                    </tr>
                    <tr>
                        <th>Total Trades</th><td class="number">{self.tester_stats.get('Total Trades', 0)}</td>
                        <th>Short Trades (won %)</th><td class="number">{short_trades_won} ({100*short_trades_won/self.tester_stats.get('Total Trades',1):.2f}%)</td>
                        <th>Long Trades (won %)</th><td class="number">{long_trades_won} ({100*long_trades_won/self.tester_stats.get('Total Trades',1):.2f}%)</td>
                    </tr>
                    <tr>
                        <th>Total Deals</th><td class="number">{self.tester_stats.get('Total Deals', 0)}</td>
                        <th>Profit Trades (% of total)</th><td class="number">{self.tester_stats.get('Profit Trades', 0)} ({100*self.tester_stats.get('Profit Trades',0)/max(self.tester_stats.get('Total Trades',1),1):.2f}%)</td>
                        <th>Loss Trades (% of total)</th><td class="number">{self.tester_stats.get('Loss Trades', 0)} ({100*self.tester_stats.get('Loss Trades',0)/max(self.tester_stats.get('Total Trades',1),1):.2f}%)</td>
                    </tr>
                    <tr>
                        <th>Largest Profit Trade</th><td class="number">{self.tester_stats.get('Largest Profit Trade', 0):.2f}</td>
                        <th>Largest Loss Trade</th><td class="number">{self.tester_stats.get('Largest Loss Trade', 0):.2f}</td>
                        <td></td><td></td>
                    </tr>
                    <tr>
                        <th>Average Profit Trade</th><td class="number">{self.tester_stats.get('Average Profit Trade', 0):.2f}</td>
                        <th>Average Loss Trade</th><td class="number">{self.tester_stats.get('Average Loss Trade', 0):.2f}</td>
                        <td></td><td></td>
                    </tr>
                    <tr>
                        <th>Max Consecutive Wins ($)</th><td class="number">{max_profit_streak_count} ({max_profit_streak_money:.2f})</td>
                        <th>Max Consecutive Losses ($)</th><td class="number">{max_loss_streak_count} ({max_loss_streak_money:.2f})</td>
                        <td></td><td></td>
                    </tr>
                    <tr>
                        <th>Max Consecutive Profit (count)</th><td class="number">{max_profit_streak_count} ({max_profit_streak_money:.2f})</td>
                        <th>Max Consecutive Loss (count)</th><td class="number">{max_loss_streak_count} ({max_loss_streak_money:.2f})</td>
                        <td></td><td></td>
                    </tr>
                    <tr>
                        <th>Average Consecutive Wins</th><td class="number">{self.tester_stats.get('Average Consecutive Wins', 0):.2f}</td>
                        <th>Average Consecutive Losses</th><td class="number">{self.tester_stats.get('Average Consecutive Losses', 0):.2f}</td>
                        <td></td><td></td>
                    </tr>
                </tbody>
            </table>
            """

        # ....
        
        # we populate table's body
        
        html = (
            template
            .replace("{{STATS_TABLE}}", stats_table)
            .replace("{{ORDER_ROWS}}", order_rows_html)
            .replace("{{DEAL_ROWS}}", deal_rows_html)
            .replace(
            "{{CURVE_IMAGE}}",
            f'<img src="{curve_img}" class="img-fluid curve-img">' if curve_img else ""
            )
        )

Beachten Sie, dass wir die Kennziffern in eine Tabelle umgewandelt haben, im Gegensatz zu dem Bericht ohne Tabelle, den der Strategietester des MetaTrader 5 erstellt. Eine Tabelle macht es einfach und bequem, einen erstellten Bericht zu lesen.

Unsere Statistik-Tabelle im Bericht sieht wie folgt aus:

Danach müssen wir eine Saldenkurve in einem Bericht darstellen.

Verwendung von matplotlib.

    def _plot_tester_curves(self, output_path: str) -> str:
        
        curves = self.tester_curves

        if not curves["time"]:
            return None

        # Convert timestamps → datetime
        times = [
            datetime.fromtimestamp(t) if isinstance(t, (int, float)) else t
            for t in curves["time"]
        ]

        plt.figure(figsize=(10, 4))

        plt.plot(times, curves["balance"], label="Balance", linewidth=2)
        plt.plot(times, curves["equity"], label="Equity", linewidth=2)
        plt.grid(visible=True, which="minor")
        # plt.plot(times, curves["margin"], label="Margin", linewidth=1, alpha=0.6)

        plt.legend(loc="upper right")
        plt.tight_layout()

        plt.savefig(output_path, dpi=150, transparent=True)
        plt.close()

        return output_path

Um Daten für unser Diagramm (Kurven) zu erhalten, müssen wir den Kontostand des Testers bei jedem Tick oder nach einigen Iterationen speichern. Bei diesem Teil müssen wir vorsichtig sein, da er leistungsmindernd sein kann, wenn er nicht korrekt ausgeführt wird.

    def __curves_update(self, time):
        
        if isinstance(time, datetime):
            time = time.timestamp()
        
        minute = int(time) // (config.CURVES_PLOT_INTERVAL_MINS*60)

        if minute == self.last_curve_minute:
            return

        self.last_curve_minute = minute

        self.tester_curves["time"].append(time)
        self.tester_curves["balance"].append(self.AccountInfo.balance)
        self.tester_curves["equity"].append(self.AccountInfo.equity)
        self.tester_curves["margin"].append(self.AccountInfo.margin)

Die obige Methode fügt die aktuelle Zeit, den Saldo, das Kapital und die Marge an die jeweiligen Arrays an, um sie später zu plotten.

    def OnTick(self, ontick_func):
        """Calls the assigned function upon the receival of new tick(s)

        Args:
            ontick_func (_type_): A function to be called on every tick
        """
        
        if not self.IS_TESTER:
            return

        self.__TesterInit()
        
        modelling = self.tester_config["modelling"]
        if modelling == "real_ticks" or modelling == "every_tick":
            
            total_ticks = sum(ticks_info["size"] for ticks_info in self.TESTER_ALL_TICKS_INFO)

            self.__GetLogger().debug(f"total number of ticks: {total_ticks}")

            with tqdm(total=total_ticks, desc="Tester Progress", unit="tick") as pbar:
                while True:
                    
                        # ...
                        
                        current_tick = utils.make_tick_from_tuple(current_tick)
                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        
                        self.__curves_update(current_tick.time)
                        ontick_func()

                        ticks_info["counter"] = counter + 1
                        any_tick_processed = True

                        pbar.update(1)

                    if not any_tick_processed:
                        break
                    
        elif modelling == "new_bar" or modelling == "1-minute-ohlc":
            
            bars_ = [bars_info["size"] for bars_info in self.TESTER_ALL_BARS_INFO]
            total_bars = sum(bars_)
            
            self.__GetLogger().debug(f"total number of bars: {total_bars}")

            with tqdm(total=total_bars, desc="Tester Progress", unit="bar") as pbar:
                while True:
                    
                    any_bar_processed = False

                    for bars_info in self.TESTER_ALL_BARS_INFO:
                        
                        # ...

                        current_tick = self._bar_to_tick(symbol=symbol, bar=bars_info["bars"].row(counter))
                        
                        self.__curves_update(current_tick["time"])
                        
                        # Getting ticks at the current bar
                        
                        self.TickUpdate(symbol=symbol, tick=current_tick)
                        ontick_func()

                        bars_info["counter"] = counter + 1
                        any_bar_processed = True

                        pbar.update(1)

                    if not any_bar_processed:
                        break
        
        self.__TesterDeinit()

Schließlich habe ich den Stop-Loss auf das Zehnfache des Take-Profits gesetzt, damit wir wissen, was wir von einem Testergebnis erwarten können – eine höhere Genauigkeit bei allen Trades. Dies ist ein guter Maßstab dafür, wie eng und effektiv unser Projekt ist.

Ausgabe:



Ich habe einen ähnlichen Handelsroboter in MQL5 erstellt:

#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;
CPositionInfo m_position;

input int magic_number = 10012026;
input int stoploss = 1000;
input int takeprofit = 100;
input int slippage = 100;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
   m_symbol.Name(Symbol());
   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetTypeFillingBySymbol(Symbol());
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
    
    if (!m_symbol.RefreshRates())
      return;
    
    double ask = m_symbol.Ask(),
           bid = m_symbol.Bid(),
           pts = m_symbol.Point();
    
    double volume = 0.01;
    
    if (!PosExists(magic_number, POSITION_TYPE_BUY))
      m_trade.Buy(volume, Symbol(), ask, ask-stoploss*pts, ask+takeprofit*pts);
    
    if (!PosExists(magic_number, POSITION_TYPE_SELL))
      m_trade.Sell(volume, Symbol(), bid, bid+stoploss*pts, bid-takeprofit*pts);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool PosExists(int magic, ENUM_POSITION_TYPE type)
 {
   for (int i=PositionsTotal()-1; i>=0; i--)
     if (m_position.SelectByIndex(i))
        if (m_position.Magic() == magic && m_position.PositionType() == type)
           return true;
           
   return false;
 }

Nach der Durchführung eines Tests in einer ähnlichen Umgebung wie bei einem nutzerdefinierten Tester ergab sich folgendes Bild.

Die Ergebnisse lagen in Bezug auf die Genauigkeit nahe beieinander, aber unser Simulator eröffnete am Ende weniger Trades als der MetaTrader 5-Strategietester; die Diskrepanz war zu erwarten, da es in unserem Projekt noch viel Raum für Verbesserungen gibt.

Teilen Sie Ihre Gedanken und helfen Sie, dieses Projekt auf GitHub zu verbessern: https://github.com/MegaJoctan/PyMetaTester


Die Quintessenz

Da es uns gelungen ist, eine vertraute Methode zur Handhabung von Ticks und Balken einzuführen und eine Schleife durch die Historie zu ziehen, während wir die Hauptfunktion einer Handelsstrategie aufrufen, wird unser Projekt nun zuverlässig.

Ein MetaTrader-5-ähnlicher Bericht des Testers ist praktisch für das Testen neuer Funktionen und die Fehlersuche in unserem nutzerdefinierten Simulator. Obwohl es fehlerhaft ist und einige Metriken fehlen, ist es immer noch besser als nichts, da wir unseren Python-Strategie-Tester weiter perfektionieren. 

Es wird noch mehr kommen; bleiben Sie dran!


Tabelle der Anhänge 

Dateiname Beschreibung und Verwendung
requirements.txt Eine Textdatei mit Informationen über alle Python-Abhängigkeiten und deren Versionen, die in diesem Projekt verwendet werden.
configs\tester.json Eine JSON-Konfigurationsdatei, die einstellbare Parameter enthält, die für den Tester verwendet werden sollen.
Reports\template.html Eine HTML-Vorlage für einen Testerbericht, der von der Klasse Tester erstellt wird.
src\bars.py Enthält Funktionen, die Balken aus dem MetaTrader 5-Terminal für Simulationszwecke sammeln.
src\ticks.py Enthält Funktionen, die Ticks vom MetaTrader 5 Terminal zu Simulationszwecken sammeln.
src\ticks_gen.py Dieses Skript verfügt über eine Klasse mit Methoden, die bei der Erzeugung von Ticks helfen, die denen des MetaTrader 5 Terminals ähneln.
Trade\Trade.py Enthält die CTrade-Klasse, eine Klasse, die die Ausführung von Trades mit MetaTrader 5-Python erleichtert.
config.py Eine Python-Konfigurationsdatei. Hier werden nützliche Variablen für das gesamte Projekt als Referenz gespeichert. 
example_bot.py Betrachten Sie diese Datei als einen Expert Advisor, der mit einem Simulator erstellt wurde, der mit MetaTrader 5-Python-Funktionen überladen ist.
tester.py  Sie enthält die Klasse Tester. Dies ist der Kern/Motor dieses Projekts. 
validators.py Es verfügt über Methoden, die uns helfen, Nutzereingaben zu validieren und vieles mehr.
utils.py  Eine Utility-Python-Datei, die wiederverwendbare Methoden für das gesamte Projekt bereithält.
Example EA.mq5  Ein MQL5-basierter Handelsroboter (EA) mit einer ähnlichen Strategie wie die in example_bot.py.

Es ist nützlich für den Vergleich der Ergebnisse des MetaTrader 5 Strategie-Testers und unseres nutzerdefinierten Testers. 

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20917

Beigefügte Dateien |
Attachments.zip (39.73 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (4)
fxsaber
fxsaber | 23 Jan. 2026 in 10:37
<img width="600" height="614" src="https://c.mql5.com/2/189/progress_bar.gif" loading="lazy" alt/ translate="no">

Für einen Forscher ist die Leistung eines Testers ein entscheidender Indikator. Es wäre gut, wenn Sie den Speicherverbrauch Ihres Testers angeben könnten.


0,2 Millionen Ticks/Sekunde ist leider eine starke Einschränkung. Vielleicht kann Numba helfen, Ihre Leistung zu verbessern.


Bitte fügen Sie Abschnitte hinzu (für unterschiedliche Anzahlen von Handelssymbolen):

benchmark_python vs benchmark_MT5tester,  (single/optimization).
RAM_python  vs RAM_MT5tester,  (single/optimization).


Vielen Dank für den Artikel!

Omega J Msigwa
Omega J Msigwa | 23 Jan. 2026 in 16:59
fxsaber #:

Für einen Forscher ist die Leistung eines Testers ein wichtiger Indikator. Es wäre gut, wenn Sie den Speicherverbrauch Ihres Testers angeben könnten.


0,2 Millionen Ticks/Sekunde sind leider eine starke Einschränkung. Vielleicht kann Numba helfen, Ihre Leistung zu verbessern.


Bitte fügen Sie Abschnitte (für unterschiedliche Anzahlen von Handelssymbolen) hinzu:


Vielen Dank für den Artikel!

Vielen Dank für die Vorschläge, ich werde in den nächsten Artikeln.

Das Ziel war, zuerst zu implementieren und dann später zu verbessern, ein langer Weg noch zu gehen😊

Richard Poster
Richard Poster | 27 Jan. 2026 in 16:46
Das ist genau das Werkzeug, nach dem ich gesucht habe! Vielen Dank. Gibt es Pläne, in Zukunft einen Parameter-Optimierer zu implementieren?
Omega J Msigwa
Omega J Msigwa | 28 Jan. 2026 in 02:43
Richard Poster #:
Das ist genau das Werkzeug, nach dem ich gesucht habe! Vielen Dank. Gibt es Pläne, in Zukunft einen Parameter-Optimierer zu implementieren?
Ja
Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Einführung in MQL5 (Teil 34): Beherrschung der API- und WebRequest-Funktion in MQL5 (VIII) Einführung in MQL5 (Teil 34): Beherrschung der API- und WebRequest-Funktion in MQL5 (VIII)
In diesem Artikel erfahren Sie, wie Sie ein interaktives Kontrollpanel in MetaTrader 5 erstellen können. Wir behandeln die Grundlagen des Hinzufügens von Eingabefeldern, Aktionsschaltflächen und Beschriftungen zur Anzeige von Text. Anhand eines projektbasierten Ansatzes werden Sie sehen, wie Sie ein Panel einrichten, in das Nutzer Nachrichten eingeben und schließlich Serverantworten von einer API anzeigen können.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Larry Williams‘ Geheimnisse des Marktes (Teil 7): Eine empirische Untersuchung zum Konzept des Handelstages der Woche Larry Williams‘ Geheimnisse des Marktes (Teil 7): Eine empirische Untersuchung zum Konzept des Handelstages der Woche
Eine empirische Untersuchung des Konzepts „Trade Day of the Week“ von Larry Williams, die zeigt, wie zeitbasierte Marktverzerrungen mit MQL5 gemessen, getestet und angewendet werden können. In diesem Artikel wird ein praktischer Rahmen für die Analyse von Gewinnquoten und Performance über Handelstage hinweg vorgestellt, um kurzfristige Handelssysteme zu verbessern.