Python-MetaTrader 5 Strategie-Tester (Teil 01): Handelssimulator
Inhalt
- Einführung
- Handels-Simulator 101
- Berechnung von Gewinnen/Verlusten aus einer Position
- Eine Position simulieren
- Handelsvalidierungen
- Ändern von Positionen
- Überwachung von Positionen
- Pending Orders im Markt
- Löschen von Pending Orders
- Ändern von Pending Orders
- Überwachen von Pending Orders
- Überwachen des Kontos
- Echtzeit-Handelssimulation in Python
- GUI-Anwendung für Echtzeitsimulationen
- Externe Verwaltung und Kontrolle von Positionen und Orders
- Umgang mit Deals
- Schlussfolgerung
Es ist besser, etwas zu tun, als nichts zu tun, während man darauf wartet, alles zu tun.
Winston Churchill.
Einführung
Das MetaTrader5-Python-Paket ist ein nützliches Modul, das es Python-Entwicklern ermöglicht, ihre Handelsanwendungen für die MetaTrader 5-Plattform zu entwickeln. Es ermöglicht Entwicklern Zugriff auf die Handelsplattform, um Daten abzurufen, Trades zu senden und diese zu überwachen.
Dieses Modul hat die Art und Weise revolutioniert, wie wir über die MetaTrader 5-Desktop-Anwendung denken. Es handelt sich nicht um eine eindimensionale Anwendung, die auf ihre native Programmiersprache zur Erstellung von Handelsrobotern, bekannt als MQL5, beschränkt ist. Diese Handelsanwendung ist flexibel genug und in der Lage, Handelsbefehle von einer externen Programmiersprache außer MQL5 zu empfangen.
Obwohl das MetaTrader5-Modul uns ermöglicht, auf der MetaTrader 5-Plattform unter Verwendung von Python Trades zu eröffnen, fehlt ihm eine entscheidende Fähigkeit, die alle MQL5-basierten Handelsanwendungen haben – die Möglichkeit, eine vollständig entwickelte Handelsanwendung im Strategy Tester zu testen.
Können Sie sich vorstellen, einen Handelsroboter zu bauen und ihn nicht testen zu können?
Zwar gibt es keinen Mangel an nützlichen Modulen in Python, da es viele nützliche Module, Bibliotheken und Frameworks zum Testen der sogenannten Handelsstrategien wie Backtrader und Backtesting.py gibt. Das Problem mit diesen Python-basierten Tools ist, dass sie zum Testen einfacher oder manchmal auch auf Indikatoren basierender Handelsstrategien entwickelt wurden.
Sie beurteilen die Handelsleistung ausschließlich anhand von Handelssignalen. Sie berücksichtigen nicht alles, was mit dem Handel zusammenhängt, wie z. B. den Broker, die Gebühren, die Beschränkungen des Handelskontos, die Eigenschaften eines bestimmten Instruments (Symbols), den Hebel des Kontos und viele weitere wichtige Details, die der MetaTrader 5 Strategietester berücksichtigt.
Das MetaTrader5-Python-Modul soll den Benutzern die grundlegenden Möglichkeiten bieten, wichtige Informationen aus der App abzurufen, und einen einfachen Einstieg in die App mithilfe der Programmiersprache Python ermöglichen.
Mit unseren Informationen und dem Wissen, wie der MetaTrader 5 Strategie-Tester funktioniert, werden wir in dieser kurzen Artikelserie eine bequeme (MetaTrader 5-Tester-ähnliche) Möglichkeit zum Testen unserer Python-basierten Handelsroboter entwickeln und implementieren.
Installieren Sie zunächst alle Python-Abhängigkeiten, die Sie in der Datei requirements.txt am Ende dieses Artikels finden.
pip install -r requirements.txt
Handels-Simulator 101
Um die Möglichkeit zu erhalten, Handelsstrategien in Python zu testen, müssen wir einen Handelssimulator erstellen. Dies ist vergleichbar mit der Funktion des MetaTrader 5 Strategy Testers, der den Markt simuliert und dabei eine Anwendung oder Funktionen (Handelsroboter oder Indikator) ausführt.
Nicht zu verwechseln: Der in MetaTrader 5 enthaltene Strategy Tester ist selbst ein Handelssimulator.
Wir werden keine grafische Benutzeroberfläche (GUI) wie beim Strategietester implementieren (zumindest vorerst), sondern eine Python-Klasse für diese Aufgabe verwenden.
import MetaTrader5 as mt5 class TradeSimulator: def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): self.mt5_instance = mt5_instance self.simulator_name = simulator_name
Das Ziel ist es, einen Klassenkonstruktor ähnlich der MetaTrader 5 Strategy Tester Konfiguration zu erhalten.

- Die Variable mt5_instance ist sehr wichtig, da sie bei der Überwachung einer ausgewählten MetaTrader 5 App-Instanz hilft.
- Die Variable simulator_name kann verwendet werden, um Ordner und Pfade zu erstellen, die bei der Unterscheidung von Handelssimulatoren helfen. Stellen Sie sich diese Variable wie den Namen eines Handelsroboters (Experten oder Indikators) vor.
In der Handelssimulatorklasse benötigen wir eine Möglichkeit, die Informationen über alle geöffneten Orders, Positionen und geschlossenen Positionen (Deals) zu verfolgen, ähnlich wie es der MetaTrader 5 macht.

class TradeSimulator: def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): # .... other variables # ... # ... # Position's information self.position_info = { "time": None, "id" : 0, "magic": 0, "symbol": None, "type": None, "volume": 0.0, "open_price": 0.0, "price": 0.0, "sl": 0.0, "tp": 0.0, "commission": 0.0, "margin_required": 0.0, "fee": 0.0, "swap": 0.0, "profit": 0, "comment": 0 } # Order's information self.order_info = self.position_info.copy() self.order_info["expiry_date"] = datetime self.order_info["expiration_mode"] = "" # Deal's information self.deal_info = self.position_info.copy() self.deal_info["reason"] = None # This is used to store the reason why the trade was closed, e.g. "Take Profit", "Stop Loss", etc. self.deal_info["direction"] = None # The only difference btn an open trade and a closed one is that the closed one has a direction showing if at that instance it was opened or closed in history # Containers for positions, orders, and deals self.positions_container = [] # a list for storing all opened trades self.deals_container = [] # a list for storing all deals self.orders_container = []
Die folgende Tabelle enthält eine Beschreibung der in der Simulatorklasse gespeicherten Positionen, Orders und Dealsinformationen.
| Eigenschaft | Beschreibung |
|---|---|
| time | Der Zeitpunkt der Positions- oder Orderausführung. Bei einem Deal ist dies der Zeitpunkt der Dealsausführung (Einstieg oder Ausstieg). |
| id | Ein eindeutig inkrementierter (Identifikator) aller Orders, Positionen oder Deals. |
| magic | Die Magic Number einer Position, einer Order oder eines Deals. |
| Symbol | Ein Instrument, mit dem der Handel eröffnet wurde, z. B.. (EURUSD, USDJPY) |
| type | Bei Positionen die Art der Position, bei Orders die Art der Order. |
| volume | Das Handelsvolumen (Losgröße) wird auf eine Position, eine Order oder einen Deal angewendet. |
| open_price | Der Eröffnungskurs einer Order oder einer Position. Dabei kann es sich entweder um den Schlusskurs oder den Eröffnungskurs eines Deals handeln, je nachdem, was der Grund für den Deal ist. |
| price | Der aktuelle Kurs auf dem Markt, er entspricht dem Briefkurs für eine Kaufposition oder kaufbezogene Pending Order und dem Geldkurs für eine Verkaufsposition oder verkaufsbezogene Pending Order. |
| sl | Der Stop-Loss-Wert einer Order, einer Position oder eines Deals. |
| tp | Der Take-Profit-Wert einer Order, einer Position oder eines Deals. |
| comission | Ermittelt die Höhe der Provision, die für eine Position berechnet wird. |
| margin_required | Speichert die erforderliche Marge für die Ausführung dieser Position oder dieser Order. |
| fee | Enthält die auf eine Position angewandten Maklergebühren. |
| swap | Speichert den Betrag des Swaps, der auf eine Position angewandt wurde. |
| profit | Speichert den berechneten Gewinn/Verlust einer Position oder eines Deals. |
| comment | Speichert den Kommentar zu einer Position, einer Order oder einem Deal. |
| expiration_mode | Speichert den SYMBOL_EXPIRATION_MODE für Pending Orders (self.order_info). |
| expiry_date | Speichert die Verfallszeit einer Order im UTC-Zeitformat. |
Alle Informationen über geöffnete Positionen, platzierte Pending Orders und ausgeführte Deals werden dann in den entsprechenden Arrays in einem Simulator gespeichert, um den Zugriff zu erleichtern.
# Containers for positions, orders, and deals self.positions_container = [] # a list for storing all opened trades self.deals_container = [] # a list for storing all deals self.orders_container = [] # for storing all pending orders placed
Berechnung von Gewinnen/Verlusten aus einer Position
Das Hauptziel der Simulation aller Handelsaktivitäten aus der Sicht eines Händlers besteht darin, Gewinne/Verluste zu ermitteln, die mit einem Handelsroboter zu einem bestimmten Zeitpunkt in der Historie erzielt werden könnten.
Im Folgenden finden Sie eine universelle Funktion für diese Aufgabe.
def _calculate_profit(self, action: str, symbol: str, entry_price: float, exit_price: float, lotsize: float) -> float: """ Calculate profit based on entry and exit prices, lot size, tick size, and tick value. Args: action (str): The action taken, either 'buy' or 'sell'. entry_price (float): The price at which the position was opened. exit_price (float): The price at which the position was closed. lotsize (float): The size of the lot in terms of contract units. """ if action != "buy" and action != "sell": print(f"Unknown order type, It can be either 'buy' or 'sell'. Received '{action}' instead.") return 0 order_type = self.mt5_instance.ORDER_TYPE_BUY if action == "buy" else self.mt5_instance.ORDER_TYPE_SELL profit = self.mt5_instance.order_calc_profit( order_type, symbol, lotsize, entry_price, exit_price ) return profit
Wir verwenden diese Funktion, um die Gewinne/Verluste aller Marktorders (Positionen) im MetaTrader 5 zu berechnen. Indem Sie einen Einstiegs- und Ausstiegskurs, ein Instrument (Symbol) und die Losgröße angeben.
if not mt5.initialize(): print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1000, leverage="1:500") profit = sim._calculate_profit(action="buy", symbol="EURUSD", entry_price=1.17246, exit_price=1.17390, lotsize=0.07) print("profit: ", profit)
Ausgabe:
(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy Tester/trade_simulator.py" profit: 10.08

Eine Position simulieren
In einer Handelssimulation ist eine Position nichts anderes als ein Bündel von berechneten Informationen, die einem Handel ähneln, der im Speicher oder auf der Festplatte gespeichert ist.
Nachfolgend finden Sie die Basisfunktion für die Öffnung von Positionen.
def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: trade_info = self.trade_info.copy() self.m_symbol.name(symbol) self.id += 1 # Increment trade ID trade_info["time"] = self.time trade_info["id"] = self.id trade_info["magic"] = self.magic_number trade_info["symbol"] = symbol trade_info["type"] = pos_type trade_info["volume"] = volume trade_info["price"] = price trade_info["sl"] = sl trade_info["tp"] = tp trade_info["commission"] = 0.0 trade_info["fee"] = 0.0 trade_info["swap"] = 0.0 trade_info["profit"] = 0.0 trade_info["comment"] = comment trade_info["margin_required"] = self._calculate_margin(symbol=symbol, volume=volume, price=price) # Append to open trades self.open_trades_container.append(trade_info) print("Trade opened successfully: ", trade_info) return True
Auch hier wird die Eigenschaft id, die dem Ticket einer Position ähnelt, automatisch inkrementiert, um eine eindeutige Ticketnummer für jede in der Klasseninstanz geöffnete Position zu erstellen.
Die Eigenschaft margin_required war bisher die größte Herausforderung, da das MetaTrader5-Modul zwar eine Funktion zur Unterstützung der Margenberechnung bereitstellt, diese aber das aktuell angemeldete Konto in der MetaTrader 5-App berücksichtigt und die Informationen dieses Kontos einschließlich des Leverage verwendet.
Da wir in diesem Python-Simulator ein simuliertes Konto haben wollen, benötigen wir eine benutzerdefinierte Funktion zur Berechnung des erforderlichen Margenwerts für jede Position entsprechend den zugewiesenen Parametern des simulierten Kontos.
def _calculate_margin(self, symbol: str, volume: float, open_price: float, margin_rate=1.0) -> float: """ Calculates margin requirement similar to MetaTrader5 based on the margin mode. """ self.m_symbol.name(symbol) if not self.m_symbol.select(): print(f"Margin calculation failed: MetaTrader5 error = {self.mt5_instance.last_error()}") return 0.0 contract_size = self.m_symbol.contract_size() leverage = self.leverage margin_mode = self.m_symbol.trade_calc_mode() print("Margin calculation mode: ",self.m_symbol.trade_calc_mode_description()) tick_size = self.m_symbol.tick_size() or 0.0001 tick_value = self.m_symbol.tick_value() or 0.0 initial_margin = self.m_symbol.margin_initial() or 0.0 face_value = self.m_symbol.trade_face_value() if margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX: margin = (volume * contract_size * margin_rate) / leverage elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE: margin = volume * contract_size * margin_rate elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFD: margin = volume * contract_size * open_price * margin_rate elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDLEVERAGE: margin = (volume * contract_size * open_price * margin_rate) / leverage elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDINDEX: margin = volume * contract_size * open_price * tick_value / tick_size * margin_rate elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX]: margin = volume * contract_size * open_price * margin_rate elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_FUTURES, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_FUTURES]: margin = volume * initial_margin * margin_rate elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX]: margin = volume * contract_size * face_value * open_price / 100 elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_SERV_COLLATERAL: margin = 0.0 else: print(f"Unknown margin mode: {margin_mode}, falling back to default margin calc.") margin = (volume * contract_size * open_price) / leverage return margin
Während die Funktion nicht perfekt ist, wenn man bedenkt, dass ich keinen Weg finden konnte, um die Variable margin_rate von der MetaTrader 5-App zu erhalten, indem ich das MetaTrader5-Python-Modul benutzte, das an den in MQL5 verwendeten Formeln zur Margenberechnung teilzunehmen scheint.
Da die Variable in symbol_info nicht verfügbar ist, bietet ein Argument namens margin_rate (standardmäßig auf 1,0 gesetzt) die Möglichkeit, diesen Wert manuell einzufügen.
Beim Speichern einer Position im Container wird angenommen, dass mit den angegebenen Parametern der Position alles in Ordnung ist. Das ist falsch, denn wie wir wissen, kann die MetaTrader 5-App prüfen, ob ein Handel bestimmte Konto-, Symbol- und Brokerdaten erfüllt, bevor er akzeptiert wird.
Die App prüft zum Beispiel, ob die Stop-Loss- und Take-Profit-Werte nicht sehr eng sind (nahe am Markt), indem sie alle Trades ablehnt, die unter diese Bedingung fallen. Sie prüft auch, ob ein Trade eine gültige Positionsgröße hat (Volumen/Lotgröße) usw.
Das heißt, wir brauchen eine Funktion, die einen booleschen Wert zurückgibt, um alle Positionen zu validieren. Nur Positionen mit allen gültigen Nachweisen werden angenommen, andernfalls werden sie abgelehnt.
Handelsvalidierungen
(a) Validierung der Losgröße
Um eine Losgröße (Volumen) des Handels zu validieren, prüfen wir drei Bedingungen.
- Wenn eine bestimmte Losgröße kleiner ist als das akzeptierte Mindestvolumen für ein Symbol
- Wenn eine bestimmte Losgröße größer ist als das maximal zulässige Volumen für ein bestimmtes Symbol
- Wenn eine bestimmte Losgröße ein Vielfaches ihrer Schrittgröße ist (minimaler Volumenänderungsschritt für die Dealsausführung)
def _position_validation(self, volume: float, symbol: str, pos_type: str, open_price: float, sl: float = 0.0, tp: float = 0.0, expiry_date: datetime = None) -> bool: """ Validates trade parameters similar to MQL5's OrderCheck() Returns: bool: True if validation passes, False with error message if fails """ self.m_symbol.name(symbol) # Assign the current symbol to the CSymbolInfo class for accessing its properties # Get symbol properties symbol_info = self.m_symbol.get_info() # Get the information about the current symbol if symbol_info is None: print(f"Trade validation failed. MetaTrader5 error = {self.mt5_instance.last_error()}") return False # Validate volume if volume < self.m_symbol.lots_min(): # check if the received lotsize is smaller than minimum accepted lot of a symbol print(f"Trade validation failed: Volume ({volume}) is less than minimum allowed ({self.m_symbol.lots_min()})") return False if volume > self.m_symbol.lots_max(): # check if the received lotsize is greater than the maximum accepted lot print(f"Trade validation failed: Volume ({volume}) is greater than maximum allowed ({self.m_symbol.lots_max()})") return False step_count = volume / self.m_symbol.lots_step() if abs(step_count - round(step_count)) > 1e-7: # check if the stoploss is a multiple of the step size print(f"Trade validation failed: Volume ({volume}) must be a multiple of step size ({self.m_symbol.lots_step()})") return False
(b): Validierung des Eröffnungskurses eines Trades und Überprüfung der Slippage
Ähnlich wie beim MetaTrader 5 Strategietester müssen wir sicherstellen, dass eine Position einen gültigen Eröffnungskurs hat, bevor wir sie akzeptieren, d. h. , der Eröffnungskurs muss nahe oder gleich dem Briefkurs eines Symbols für eine Kaufposition sein, und der Eröffnungskurs muss nahe oder gleich dem Geldkurs für eine Verkaufsposition sein.
Ein Slippage-Wert (sofern angegeben) wird nur für Preisvergleiche verwendet, um sicherzustellen, dass ein bestimmter Einstiegskurs nahe am Geldkurs liegt.
# Validate the opening price self.m_symbol.refresh_rates() # Get recent ticks information ask = self.m_symbol.ask() bid = self.m_symbol.bid() if ask is None or bid is None or ask==0 or bid==0: print("Trade Validate: Failed to Get Ask and Bid prices, Call the function market_update() to update the simulator with newly simulated price values") return False # Slippage check actual_price = ask if pos_type == "buy" else bid point = self.m_symbol.point() # Allowable slippage range (in absolute price) max_deviation = self.deviation_points * point lower_bound = actual_price - max_deviation upper_bound = actual_price + max_deviation # Check if requested price is within allowed slippage if not (lower_bound <= open_price <= upper_bound): print(f"Trade validation failed: {pos_type.capitalize()} price ({open_price}) is out of slippage range: {lower_bound:.5f} - {upper_bound:.5f}") return False
(c): Validierung von Stop Loss und Take Profit
Nicht alle Stop-Loss- und Take-Profit-Werte von Marktorders (Positionen) werden von MetaTrader 5-Brokern akzeptiert; einige Stop-Loss- und Take-Profit-Werte sind möglicherweise ungültig oder zu nah am Markt, um eine Position zu eröffnen.
Wir verwenden dieselbe Logik für die Überprüfung der Stop- und Freeze-Niveaus.
Erstens prüfen wir, ob überhaupt ein angemessener Stop-Loss eingegangen ist.
Dann stellen wir sicher, dass der Stop-Loss-Wert unter dem Eröffnungskurs der Position und der Take-Profit-Wert über dem Eröffnungskurs liegt, wenn es sich um einen Kauf handelt. Bei einem Verkaufsgeschäft verfahren wir genau umgekehrt (der Stop-Loss muss über dem Eröffnungskurs liegen und der Take-Profit muss darunter liegen).
# Validate stop loss and take profit levels if sl > 0: if pos_type == "buy" and sl >= open_price: print(f"Trade validation failed: Buy stop loss ({sl}) must be below order opening price ({open_price})") return False if pos_type == "sell" and sl <= open_price: print(f"Trade validation failed: Sell stop loss ({sl}) must be above order opening price ({open_price})") return False if not self._check_stop_level(symbol, open_price, sl, pos_type): return False if tp > 0: if pos_type == "buy" and tp <= open_price: print(f"Trade validation failed: Buy take profit ({tp}) must be above order opening price ({open_price})") return False if pos_type == "sell" and tp >= open_price: print(f"Trade validation failed: Sell take profit ({tp}) must be below order opening price ({open_price})") return False if not self._check_stop_level(symbol, open_price, tp, pos_type): return False
Die obigen Codezeilen sind in der Funktion _check_stops_level zu finden.
def _check_stop_level(self, symbol: str, price: float, stop_price: float, pos_type: str) -> bool: """Check if stop levels comply with broker requirements""" self.m_symbol.name(symbol) # Validate symbol if not self.m_symbol.select(): print(f"Failed to check stop level: Symbol {symbol}. MetaTrader5 error = {self.mt5_instance.last_error()}") return False # Check for stops level stop_level = self.m_symbol.stops_level() if pos_type == "buy": if stop_price > price - stop_level * self.m_symbol.point(): print(f"Trade validation failed: Stop level too close. Must be at least {stop_level} points away") return False else: # sell if stop_price < price + stop_level * self.m_symbol.point(): print(f"Trade validation failed: Stop level too close. Must be at least {stop_level} points away") return False # Check for freeze level freeze_level = self.m_symbol.freeze_level() if pos_type == "buy": if stop_price > price - freeze_level * self.m_symbol.point(): print(f"Trade validation failed: Stop level too close. Must be at least {freeze_level} points away") return False else: # sell if stop_price < price + freeze_level * self.m_symbol.point(): print(f"Trade validation failed: Stop level too close. Must be at least {freeze_level} points away") return False return True
Die obige Funktion gibt False zurück, wenn bei einer Kauf- oder Verkaufsposition ein ungültiger Stop-Loss- oder Take-Profit-Wert festgestellt wurde. Andernfalls gibt sie True zurück.
Schließlich rufen wir die Funktion _position_validation innerhalb der Basisfunktion für die Eröffnung von Positionen auf. Es prüft die Gültigkeit einer Position, bevor es sie in einem Array mit allen Positionen speichert.
def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: trade_info = self.trade_info.copy() self.m_symbol.name(symbol) if not self._position_validation(volume=volume, symbol=symbol, pos_type=pos_type, price=price, sl=sl, tp=tp): return False self.id += 1 # Increment trade ID trade_info["time"] = self.time trade_info["id"] = self.id # ... proceeds to store a trade # Append to open trades self.open_trades_container.append(trade_info) print("Trade opened successfully: ", trade_info) return True
Um das Eröffnen von Kauf- und Verkaufspositionen zu vereinfachen, habe ich zwei spezielle Funktionen namens Kaufen und Verkaufen für das Eröffnen von Kauf- bzw. Verkaufspositionen erstellt. Diese beiden Funktionen beruhen auf der Basisfunktion _open_position. Der einzige Unterschied zwischen diesen beiden Funktionen und ihren Vorgängern ist eine Variable namens pos_type (zur Festlegung des Typs einer Position). Dieser Wert wird explizit in den folgenden Funktionen verwendet.
def buy(self, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: return self._open_position("buy", volume, symbol, price, sl, tp, comment) def sell(self, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: return self._open_position("sell", volume, symbol, price, sl, tp, comment)
Die obigen Funktionen wurden von ähnlichen Funktionen inspiriert, die in der Klasse CTrade verfügbar sind, die von Standard-Handelsbibliotheken in der Sprache MQL5 angeboten wird.
Ändern von Positionen
Die Möglichkeit, Ihre Positionen zu ändern, ist aus verschiedenen Gründen des Handels und der Geldverwaltung von entscheidender Bedeutung. Beispielsweise ändern Händler häufig die Stop-Loss-Werte in Positionen, indem sie sie in Richtung eines Einstiegs oder des Take-Profit-Wertes verschieben, um Verluste zu reduzieren oder einen Gewinn zu erzielen; dies wird als Trailing-Stop oder Breakeven bezeichnet.
Nachfolgend finden Sie eine Funktion, die Python-Entwickler bei der Änderung von Positionen in einem Simulator unterstützt.
def position_modify(self, pos: dict, new_sl: float, new_tp) -> bool: new_position = pos.copy() if pos["type"] == "buy": if new_sl >= pos["price"]: print("Failed to modify sl, new_sl >= current price") return False if pos["type"] == "sell": if new_sl <= pos["price"]: print("Failed to modify sl, new_sl <= current price") return False if not self._check_stops_level(symbol=pos["symbol"], open_price=pos["open_price"], stop_price=new_sl, pos_type=pos["type"]): print("Failed to Modify the Stoploss") if not self._check_stops_level(symbol=pos["symbol"], open_price=pos["open_price"], stop_price=new_tp, pos_type=pos["type"]): print("Failed to Modify the Takeprofit") # new sl and tp values new_position["sl"] = new_sl new_position["tp"] = new_tp # Update the position in a container for i, p in enumerate(self.positions_container): if p["id"] == pos["id"]: self.positions_container[i] = new_position print(f"Position with id=[{pos['id']}] modified! new_sl={new_sl} new_tp={new_tp}") return True print("Failed to modify position: ID not found") return True
Der Prozess der Änderung einer Position im MetaTrader 5 hat einige ähnliche Aspekte wie die Eröffnung einer neuen Position; die oben genannte Funktion stellt sicher, dass zwei Prüfungen erfüllt sind, bevor die Änderung der Position bestätigt wird.
- Überprüfung, ob der neu gesetzte Stop-Loss entsprechend der Art der Position gültig ist, d.h., der neue Stop-Loss-Wert muss bei einer Kaufposition größer sein als der aktuelle Marktpreis der Position und bei einer Verkaufsposition das Gegenteil.
- Sicherstellen, dass neue Stop-Loss- oder Take-Profit-Werte nicht zu nahe am Markt liegen.
Beispiel für die Verwendung:
Eröffnen wir eine einfache Kaufposition und ändern wir ihren Stop-Loss. Nach jeweils einer Sekunde erhöhen wir den Stop-Loss einer solchen Position um 0,005.
stoploss = 500 ask = m_symbol.ask() point = m_symbol.point() sim.buy(volume=0.1, symbol=symbol, open_price=ask, sl=ask-stoploss*point) while True: # constantly monitor trades and account metrics sim.monitor_pending_orders() sim.monitor_positions(verbose=False) for pos in sim.get_positions(): # go through all positions, same as in MQL5 if pos["type"] == "buy" and pos["symbol"] == symbol: # select a buy position for the current symbol sim.position_modify(pos=pos, new_sl=pos["sl"]-0.005, new_tp=pos["tp"]) sim.run_toolbox_gui() # Run the simulator toolbox GUI time.sleep(5) # sleep for one second
Ausgabe:
Position with id=[1] modified! new_sl=1.1320700000000001 new_tp=0.0 Position with id=[1] modified! new_sl=1.1270700000000002 new_tp=0.0 Position with id=[1] modified! new_sl=1.1220700000000003 new_tp=0.0 Position with id=[1] modified! new_sl=1.1170700000000005 new_tp=0.0
Überwachung von Positionen
Da eine Position nichts anderes als ein Bündel von Informationen ist, die vorübergehend im Speicher abgelegt werden, müssen diese Informationen ständig aktualisiert werden.
Zum Beispiel müssen wir nach der Eröffnung einer Position deren laufenden Gewinn oder Verlust entsprechend den Preisbewegungen auf dem Markt (jüngste Geld- und Briefkurse) aktualisieren, ganz zu schweigen davon, dass wir den Ausstieg jeder Position überwachen müssen, d.h., wenn der aktuelle Preis auf dem Markt (Geldkurs für eine Kaufposition oder Briefkurs für eine Verkaufsposition) entweder dem Stop-Loss oder dem Take-Profit eines Handels entspricht, schließen wir diesen Handel.
(a): Überwachung des Gewinns des Handels
Mithilfe der zuvor besprochenen Funktion zur Berechnung der Gewinne einer Position überwachen und aktualisieren wir ständig die Gewinne jeder Position.
def monitor_positions(self, verbose: bool): # monitoring all open trades for pos in self.positions_container: self.m_symbol.name(pos["symbol"]) self.m_symbol.refresh_rates() # Get ticks information for every symbol ask = self.m_symbol.ask() bid = self.m_symbol.bid() # update price information on all positions pos["price"] = ask if pos["type"] == "buy" else bid # Monitor and calculate the profit of a position pos["profit"] = self._calculate_profit(action=pos["type"], symbol=pos["symbol"], lotsize=pos["volume"], entry_price=pos["open_price"], exit_price=(ask if pos["type"]=="buy" else bid))
(b): Überwachung der Ausgänge von Positionen
Nachdem eine Position im Strategietester eröffnet wurde, mit oder ohne vorgegebene Ziele (Stop-Loss- und Take-Profit-Werte), wird sie nicht selbst geschlossen.
Wir müssen ihn ständig überwachen, indem wir prüfen, ob der aktuelle Marktpreis (Nachfrage für eine Verkaufsposition und Angebot für eine Kaufposition) ein solches gewünschtes Ziel erreicht hat. Wenn sie eines der Positionsziele erreicht hat, wird diese Position geschlossen und ein Deal zur Dealshistorie hinzugefügt.
def monitor_positions(self, verbose: bool): # monitoring all open trades for pos in self.positions_container: self.m_symbol.name(pos["symbol"]) self.m_symbol.refresh_rates() # Get ticks information for every symbol ask = self.m_symbol.ask() bid = self.m_symbol.bid() # ... other monitors # Monitor the stoploss and takeprofit situation of positions if pos["tp"] > 0 and ((pos["type"] == "buy" and bid >= pos["tp"]) or (pos["type"] == "sell" and ask <= pos["tp"])): # Take profit hit self.position_close(pos_id=pos) # close such position if pos["sl"] > 0 and ((pos["type"] == "buy" and bid <= pos["sl"]) or (pos["type"] == "sell" and ask >= pos["sl"])): # Stop loss hit self.position_close(pos_id=pos) # close such position
Schließlich beabsichtigen wir, einige Informationen über jede Position ausdrucken, sobald sie aktualisiert wird, ähnlich wie es die MetaTrader 5 Terminal Toolbox tut (sie zeigt uns aktive Positionen).
Nur wenn die Variable mit dem Namen verbose = True ist.
# Print the information about all trades (positions and orders (if any)) if verbose: print(f'sim -> ticket | {trade["id"]} | symbol {trade["symbol"]} | time {trade["time"]} | type {trade["type"]} | volume {trade["volume"]} | sl {trade["sl"]} | tp {trade["tp"]} | profit {trade["profit"]:.2f}')
Im Moment überwachen wir nur Kauf- und Verkaufspositionen; wir werden in Kürze auch die Überwachung von Pending Orders besprechen.
Pending Orders im Markt
Im Gegensatz zu Marktorders (Positionen), die für eine sofortige Ausführung am Markt vorgesehen sind, enthalten Pending Orders einen verpflichtenden Auftrag (eine Handelsoperation) bei Vorliegen einer bestimmten Bedingung. Pending Orders können auch eine zeitliche Beschränkung für ihre Aktionen enthalten – das Ablaufdatum der Order.
Zu den Pending Orders gehören.
Für den Anfang werden wir die ersten vier Typen von Pending Orders aus der obigen Liste in der Handelssimulatorklasse implementieren, nur um den Einstieg zu erleichtern.
Beginnend mit der Basisfunktion für die Platzierung von Pending Orders.
Die Überprüfungen:
(a): Prüfen, ob die Orderart korrekt ist.
def _place_a_pending_order(self, order_type: str, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc" ): order_types = ["buy limit", "buy stop", "sell limit", "sell stop"] if order_type not in order_types: raise ValueError(f"Invalid pending order type, available order types include: {order_types}") expiration_modes = ["gtc", "daily", "daily_excluding_stops"] if expiration_mode not in expiration_modes: raise ValueError(f"Invalid Expiration mode, available modes include: {expiration_modes}")
(b): Sicherstellen, dass alle Pending Orders nicht zu nah am Markt sind.
- Sicherstellen, dass der Eröffnungskurs einer kaufbezogenen Pending Order nicht zu nahe am Geldkurs liegt.
- Sicherstellen, dass der Eröffnungskurs einer verkaufsbezogenen Pending Order nicht zu nahe am Briefkurs liegt.
# Get market info self.m_symbol.name(symbol_name=symbol) # assign symbol's name self.m_symbol.refresh_rates() # get recent ticks from the market using the current selected symbol if order_type in ("buy limit", "buy stop"): if abs(open_price - self.m_symbol.bid()) < self.m_symbol.stops_level() * self.m_symbol.point(): print(f"Failed to open a pending order, a '{order_type}' order is too close to the market") if order_type in ("sell limit", "sell stop"): if abs(open_price - self.m_symbol.ask()) < self.m_symbol.stops_level() * self.m_symbol.point(): print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
(c): Sicherstellen, dass ein gültiges Ablaufdatum für die Orders erhalten wird
Das Verfallsdatum oder die Verfallszeit muss ein Zeitwert sein, der größer ist als der aktuelle Zeitpunkt – ein Zeitpunkt in der Zukunft.
# check if the order has a valid expiry date if expiry_date is not None: # if an expiry date is given in the first place if expiry_date <= self.m_symbol.time(timezone=pytz.UTC): print(f"Failed to place a pending order {order_type}, Invalid datetime") return
Nachdem eine Order die drei Prüfungen bestanden hat, wird sie in die Liste der in der Klasse gespeicherten Orders aufgenommen.
order_info = self.order_info.copy() self.id += 1 order_info["id"] = self.id order_info["type"] = order_type order_info["volume"] = volume order_info["symbol"] = symbol order_info["open_price"] = open_price order_info["sl"] = sl order_info["tp"] = tp order_info["comment"] = comment order_info["magic"] = self.magic_number order_info["margin_required"] = self._calculate_margin(symbol=symbol, volume=volume, open_price=open_price) order_info["expiry_date"] = expiry_date order_info["expiration_mode"] = expiration_mode self.orders_container.append(order_info) # add a valid order to it's container
Wir inkrementieren dieselbe ID (Ticketnummer), die bei der Einstellung der Positions-ID verwendet wird, auch bei der Platzierung von Pending Orders, denn eine Pending Order ist eine Position in Verkleidung (es ist eine Position, die darauf wartet, eröffnet zu werden, und jede Position war einmal eine Order).
Die Verwendung der gleichen ID hilft, doppelte ID-Nummern in den ausgelösten Positionen zu vermeiden.
Mithilfe dieser Basisfunktion können wir bequeme/getrennte Funktionen für die Platzierung von Pending Orders implementieren.
Platzierung einer Buy Stop Order:
def buy_stop(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None,expiration_mode: str="gtc"): # validate an order according to it's type self.m_symbol.name(symbol_name=symbol) self.m_symbol.refresh_rates() if self.m_symbol.bid() >= open_price: print("Failed to place a buy stop order, open price <= the bid price") return self._place_a_pending_order("buy stop", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)
Platzierung einer Buy Limit Order:
def buy_limit(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"): self.m_symbol.name(symbol_name=symbol) self.m_symbol.refresh_rates() if self.m_symbol.bid() <= open_price: print("Failed to place a buy limit order, open price >= current bid price") return self._place_a_pending_order("buy limit", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)
Platzierung einer Sell Stop-Order:
def sell_stop(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"): self.m_symbol.name(symbol_name=symbol) self.m_symbol.refresh_rates() if self.m_symbol.ask() <= open_price: print("Failed to place a sell stop order, open price >= current ask price") return self._place_a_pending_order("sell stop", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)
Platzierung einer Sell Limit-Order:
def sell_limit(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"): self.m_symbol.name(symbol_name=symbol) self.m_symbol.refresh_rates() if self.m_symbol.ask() >= open_price: print("Failed to place a sell limit order, open price <= current ask price") return self._place_a_pending_order("sell limit", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)
In den obigen Funktionen fügen wir Bedingungen hinzu, um sicherzustellen, dass jede Order zumindest an der richtigen Stelle platziert wird, d. h,
- Eine Buy Stop Order wird oberhalb des aktuellen Marktpreises (Briefkurs) platziert
- Ein Buy Limit Order wird unter dem aktuellen Marktpreis (Geldkurs) platziert.
- Ein Sell Stop-Order wird unter dem aktuellen Marktpreis (Geldkurs) platziert.
- Ein Sell Limit-Order wird oberhalb dem aktuellen Marktpreis (Briefkurs) platziert.
Pending Orders löschen
Eine Funktion, die für das Löschen von Pending Orders zuständig ist, ist ebenso wichtig wie eine Funktion, die Pending Orders platziert.
Bei dieser Funktion sind keine Prüfungen erforderlich, und es wird kein Deal gespeichert, wenn eine Order gelöscht wird.
def order_delete(self, selected_order: dict) -> bool: # delete a pending order from the orders container if selected_order in self.orders_container: self.orders_container.remove(selected_order) return True else: print(f"Warning: An Order with ID {selected_order['id']} not found!") return False
Ändern von Pending Orders
Wir brauchen auch eine Funktion für die Änderung von Pending Orders, ähnlich wie wir eine Funktion für die Änderung von Positionen haben.
Es gibt drei wichtige Prüfungen, die innerhalb einer Funktion für diese Aufgabe erforderlich sind.
(a): Die Prüfung, ob der neue Eröffnungskurs einer Position an der richtigen Stelle je nach Orderart liegt.
Bei allen Pending Orders muss der neue Eröffnungskurs angegeben werden:
- Oberhalb des aktuellen Marktpreises (Briefkurs) für eine Buy Stop Order
- Unter dem aktuellen Marktpreis (Geldkurs) für eine Buy Limit Order
- Unterhalb des aktuellen Marktpreises (Geldkurs) für eine Sell Stop Order
- Oberhalb des aktuellen Marktpreises (Briefkurs) für eine Sell Limit Order
def order_modify(self, order: dict, new_open_price: float, new_sl: float, new_tp: float, new_expiry: datetime = None, new_expiration_mode: str = None): """ Modify an existing pending order's open price, SL/TP, and optionally its expiration settings. """ new_order = order.copy() # Validate order type valid_types = ["buy limit", "buy stop", "sell limit", "sell stop"] if order["type"] not in valid_types: print(f"Invalid order type for modification: {order['type']}") return False self.m_symbol.name(order["symbol"]) self.m_symbol.refresh_rates() # Ensure open price is placed logically according to type ask = self.m_symbol.ask() bid = self.m_symbol.bid() if order["type"] == "buy stop" and bid >= new_open_price: print("Failed to modify Buy Stop: new open price <= current bid price") return False if order["type"] == "buy limit" and bid <= new_open_price: print("Failed to modify Buy Limit: new open price >= current bid price") return False if order["type"] == "sell stop" and ask <= new_open_price: print("Failed to modify Sell Stop: new open price >= current ask price") return False if order["type"] == "sell limit" and ask >= new_open_price: print("Failed to modify Sell Limit: new open price <= current ask price") return False
(b): Die Prüfung, ob der Eröffnungspreis der neuen Order nicht sehr nahe am Markt liegt.
# ensure the order ins't close to the market order_type = order["type"] if order_type in ("buy limit", "buy stop"): if abs(new_open_price - self.m_symbol.bid()) < self.m_symbol.stops_level() * self.m_symbol.point(): print(f"Failed to open a pending order, a '{order_type}' order is too close to the market") return False if order_type in ("sell limit", "sell stop"): if abs(new_open_price - self.m_symbol.ask()) < self.m_symbol.stops_level() * self.m_symbol.point(): print(f"Failed to open a pending order, a '{order_type}' order is too close to the market") return False
(c): Die Überprüfung, ob die Verfallszeit der neu erteilten Order angemessen ist
if new_expiry and new_expiry <= self.m_symbol.time(timezone=pytz.UTC): print("Invalid Expiry date, new expiry date must be a value in the future")
Schließlich ändern und aktualisieren wir alle Orders im Container.
# Update the order in the container for i, o in enumerate(self.orders_container): if o["id"] == order["id"]: self.orders_container[i] = new_order print(f"Order with id=[{order['id']}] modified successfully.") return True print("Failed to modify order: ID not found") return False
Überwachung der Pending Orders
Ähnlich wie eine Position ist eine Order nichts anderes als ein Bündel von Informationen, die in einer Liste von Wörterbüchern in der Klasse gespeichert sind. Sobald eine Order gespeichert ist, muss sie ständig überwacht werden, d.h., es muss ein Code vorhanden sein, der prüft, ob der aktuelle Preis (Brief- oder Geldkurs) den Eröffnungskurs einer Pending Order erreicht hat; sobald der aktuelle Marktpreis den Eröffnungskurs der Order erreicht, wird er ausgelöst und stattdessen zu einer Liste offener Positionen hinzugefügt.
Wir müssen auch die Situation der Verfallszeit in allen Pending Orders mit einem Verfallsdatum und dem richtigen Verfallsmodus überwachen. Mehr dazu.
def monitor_pending_orders(self): now = datetime.now(tz=pytz.UTC) expired_orders = [] triggered_orders = [] for order in self.orders_container: # loop through all orders expiration_mode = order.get("expiration_mode", "gtc") expiry_date = order.get("expiry_date") # Check for expiration based on mode if expiration_mode == "daily" or expiration_mode == "daily_excluding_stops": if expiry_date and now >= expiry_date: expired_orders.append(order) continue # Skip to next order self.m_symbol.name(symbol_name=order["symbol"]) if not self.m_symbol.refresh_rates(): continue ask = self.m_symbol.ask() bid = self.m_symbol.bid() open_price = order["open_price"] order_type = order["type"].lower() if order_type in ("buy limit", "buy stop"): order["price"] = self.m_symbol.ask() if order_type in ("sell limit", "sell stop"): order["price"] = self.m_symbol.bid() triggered = False # store the triggered condition of an order if order_type == "buy limit" and ask <= open_price: triggered = self.buy(order["volume"], order["symbol"], ask, order["sl"], order["tp"], order["comment"]) # open a buy position with credentials taken from an order elif order_type == "buy stop" and ask >= open_price: triggered = self.buy(order["volume"], order["symbol"], ask, order["sl"], order["tp"], order["comment"]) # open a buy position elif order_type == "sell limit" and bid >= open_price: triggered = self.sell(order["volume"], order["symbol"], bid, order["sl"], order["tp"], order["comment"]) # open a sell position elif order_type == "sell stop" and bid <= open_price: triggered = self.sell(order["volume"], order["symbol"], bid, order["sl"], order["tp"], order["comment"]) # open a sell position if triggered: triggered_orders.append(order) # add a triggerd order to the list # Clean up expired and triggered orders for order in expired_orders + triggered_orders: if order in self.orders_container: self.orders_container.remove(order)
Überwachung des Kontos
Nach der Überwachung aller Positionen und der Aktualisierung ihrer Anmeldedaten (einschließlich ihrer Gewinn-/Verlustwerte) müssen wir auch unsere Kontodaten aktualisieren, d. h. den Kontostand entsprechend der Einlage, dem Eigenkapital, der Marge, der freien Marge und der Margenhöhe des Simulators – all diese Kontodaten hängen von den Handelsaktivitäten ab.

Ein simuliertes Konto wird in einer Funktion namens monitor_account überwacht:
| Eigenschaften des Kontos | Berechnung | Beschreibung |
|---|---|---|
| Laufende Gewinn-/Verlustberechnung | unrealized_pl = sum(pos['profit'] or 0 for pos in self.positions_container) self.account_info["profit"] = unrealized_pl | Berechnet die Summe der Gewinne aus allen geöffneten Positionen im Simulator. |
| Aktualisierung des Kontoeigenkapitals (equity) des Kontos | self.account_info['equity'] = self.account_info['balance'] + unrealized_pl | Das Kontoeigenkapital ergibt sich aus der Summe der Gewinne und Verluste aus allen Positionen, wenn man sie vom Kontostand abzieht. |
| Verwendete Marge | self.account_info['margin'] = sum(pos['margin_required'] or 0 for pos in self.positions_container) | Die gesamte verbrauchte Marge ist die Summe der Margen, die von allen Positionen verbraucht werden. |
| Freie Marge | self.account_info['free_margin'] = self.account_info['equity'] - self.account_info['margin'] | Die freie Marge ist die Differenz zwischen dem Kontoeigenkapital und der gesamten auf dem Konto verwendeten Marge. |
| Höhe der Marge | self.account_info['margin_level'] = (self.account_info['equity'] / self.account_info['margin']) * 100 \ if self.account_info['margin'] > 0 else 0.0 | Entspricht dem Kontoeigenkapital geteilt durch die Marge des Kontos in Prozent, nur wenn die verwendete Marge größer als Null ist (Marge > 0). |
Schließlich drucken wir die Anmeldedaten des Kontos am Ende der Funktion monitor_account aus.
Nur wenn das Argument verbose = True lautet.
def monitor_account(self, verbose: bool): """Recalculates all account metrics based on current positions""" # 1. Calculate unrealized P/L unrealized_pl = sum(pos['profit'] or 0 for pos in self.open_trades_container) self.account_info["profit"] = unrealized_pl # 2. Update Equity (Balance + Floating P/L) self.account_info['equity'] = self.account_info['balance'] + unrealized_pl # 3. Calculate Used Margin self.account_info['margin'] = sum(pos['margin_required'] or 0 for pos in self.open_trades_container) # 4. Calculate Free Margin (Equity - Used Margin) self.account_info['free_margin'] = self.account_info['equity'] - self.account_info['margin'] # 5. Calculate Margin Level (Equity / Margin * 100) self.account_info['margin_level'] = (self.account_info['equity'] / self.account_info['margin']) * 100 \ if self.account_info['margin'] > 0 else 0.0 if verbose: print(f"Balance: {self.account_info['balance']:.2f} | Equity: {self.account_info['equity']:.2f} | Profit: {self.account_info['profit']:.2f} | Margin: {self.account_info['margin']:.2f} | Free margin: {self.account_info['free_margin']} | Margin level: {self.account_info['margin_level']:.2f}%")
Der Kontostand wird nur aktualisiert, wenn ein Handel geschlossen wird, was uns zu der Funktion position_close zurückführt.
def position_close(self, selected_pos: dict) -> bool: # Update deal info deal_info = selected_pos.copy() deal_info["direction"] = "closed" # check if the reason was SL or TP according to recent tick/price information self.m_symbol.name(selected_pos["symbol"]) self.m_symbol.refresh_rates() ask = self.m_symbol.ask() bid = self.m_symbol.bid() digits = self.m_symbol.digits() deal_info["reason"] = "Unknown" # Unkown deal reason if the stoploss or takeprofit wasn't hit if selected_pos["type"] == "buy": if np.isclose(selected_pos["tp"], bid, digits): # check if the current bid price is almost equal to the takeprofit deal_info["reason"] = "Take profit" elif np.isclose(selected_pos["sl"], bid, digits): # check if the current bid price is almost equal to the stoploss deal_info["reason"] = "Stop loss" if selected_pos["type"] == "sell": if np.isclose(selected_pos["tp"], ask, digits): # check if the current ask price is almost equal to the takeprofit deal_info["reason"] = "Take profit" elif np.isclose(selected_pos["sl"], ask, digits): # check if the current ask price is almost equal to the stoploss deal_info["reason"] = "Stop loss" self.deals_container.append(deal_info.copy()) # add the deal to the deals container print("Trade closed successfully: ", deal_info) # Save closed deal to database self._save_closed_deal(deal_info, self.history_db_name) # Remove trade from open positions if selected_pos in self.open_trades_container: # update the account balance self.account_info["balance"] += selected_pos["profit"] self.open_trades_container.remove(selected_pos) else: print(f"Warning: Position with ID {selected_pos['id']} not found!") return True
Echtzeit-Handelssimulation in Python
Da wir die Möglichkeit haben, innerhalb der Klasse TradeSimulator Trades zu eröffnen und Handelsaktivitäten zu überwachen, wollen wir unsere ersten Trades in der Simulation sowie die echten Trades in der MetaTrader 5 Desktop-App öffnen. Ziel ist es, Ähnlichkeiten zwischen den Handelsaktivitäten in zwei unterschiedlichen Umfeldern zu finden.
Bevor wir den Handel eröffnen, müssen wir uns über die Methoden im Klaren sein, mit denen wir wichtige Handelsparameter in einem Simulator konfigurieren.
class TradeSimulator: def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): #... other functions def set_magicnumber(self, magic_number: int): self.magic_number = magic_number def set_deviation_in_points(self, deviation_points: int): self.deviation_points = deviation_points
Die Funktion set_magicnumber legt die Magic Number für alle Trades im Simulator fest, während die Funktion set_deviation_in_points die Slippage für alle Abschlüsse in der Klasse festlegt.
Nachdem wir alle notwendigen Module in die Datei simulator_test.py importiert haben, initialisieren wir die MetaTrader 5-Desktop-Anwendung mithilfe des MetaTrader5-Moduls.
import MetaTrader5 as mt5 from Trade.SymbolInfo import CSymbolInfo from Trade.Trade import CTrade from datetime import datetime import time import pytz from trade_simulator import TradeSimulator if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit()
Es folgt die Initialisierung der Klasse TradeSimulator.
sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") magic_number = 123456 slippage = 10 sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator
Wir werden die in diesem Artikel besprochene Klasse CTrade verwenden, um dieselben Trades in MetaTrader 5 zu öffnen. Wir werden die in einem Simulator geöffneten Trades mit den in MetaTrader 5 geöffneten vergleichen.
m_trade = CTrade() # Initializing the CTrade class symbol = "EURUSD" m_trade.set_magicnumber(magic_number=magic_number) # sets the magic number of the CTrade class m_trade.set_deviation_in_points(deviation_points=slippage) # sets slippage m_trade.set_filling_type_by_symbol(symbol=symbol) #set filling type by the given symbol
Wir eröffnen dieselben Trades sowohl in einem Handelssimulator als auch in MetaTrader 5.
m_symbol = CSymbolInfo(mt5_instance=mt5) m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader 5 print("failed to get recent ticks data") sim.monitor_account(verbose=True) # calculate account credentials initially # Open trades in a Simulator lotsize = 0.01 if not sim.buy(volume=lotsize, symbol=symbol, open_price=m_symbol.ask(), sl=0.0, tp=0.0, comment="Test Buy Trade"): print("Failed to simulate a trade") if not sim.sell(volume=lotsize, symbol=symbol, open_price=m_symbol.bid(), sl=0.0, tp=0.0, comment="Test Sell Trade"): print("Failed to simulate a trade") # Open trades in MetaTrader5 if not m_trade.buy(volume=lotsize, symbol=symbol, price=m_symbol.ask(), sl=0.0, tp=0.0, comment="Test Buy Trade"): print("Failed to open a trade in MetaTrader5") if not m_trade.sell(volume=lotsize, symbol=symbol, price=m_symbol.bid(), sl=0.0, tp=0.0, comment="Test Buy Trade"): print("Failed to open a trade in MetaTrader5")
Innerhalb einer Endlosschleife wollen wir alle Positionen und ein Konto in einer Simulation überwachen.
while True: # constantly monitor trades and account metrics sim.monitor_account(verbose=True) sim.monitor_positions(verbose=True) time.sleep(1) # sleep for one second
Ausgabe:

Das ist sehr unangenehm anzuschauen. Lassen Sie uns eine einfache GUI-Anwendung erstellen, um diese Handelsaktivität in Python zu visualisieren.
GUI-Anwendung für Echtzeitsimulationen
Für diese einfache Anwendung verwenden wir das tkinter-Modul.
import tkinter as tk from tkinter import ttk
from datetime import datetime class SimToolboxGUI: def __init__(self): self.root = tk.Tk() self.root.title("Trade Simulator Monitor") self.root.geometry("900x700") self.root.configure(bg="#f0f0f0") # === ACCOUNT INFO DISPLAY === self.account_label = tk.Label( self.root, text="", font=("Courier", 8), anchor="w", justify="left", bg="#f0f0f0", fg="#333", ) self.account_label.pack(fill="x", padx=5, pady=(5, 6)) # === POSITION TABLE === position_frame = tk.LabelFrame(self.root, text="Open Positions", bg="#f0f0f0") position_frame.pack(fill="both", expand=True, padx=10, pady=5) self.position_columns = [ "id", "symbol", "time", "type", "volume", "open_price", "sl", "tp", "swap", "price", "profit", "comment" ] self.position_tree = ttk.Treeview(position_frame, columns=self.position_columns, show="headings", height=10) for col in self.position_columns: self.position_tree.heading(col, text=col) self.position_tree.column(col, anchor="center", width=80) self.position_tree.pack(fill="both", expand=True, padx=5, pady=5) vsb1 = ttk.Scrollbar(position_frame, orient="vertical", command=self.position_tree.yview) self.position_tree.configure(yscrollcommand=vsb1.set) vsb1.pack(side="right", fill="y") # === ORDER TABLE === order_frame = tk.LabelFrame(self.root, text="Pending Orders", bg="#f0f0f0") order_frame.pack(fill="both", expand=True, padx=10, pady=5) self.order_columns = [ "id", "symbol", "time", "type", "volume", "open_price", "sl", "tp", "price", "expiry_date", "expiration_mode", "comment" ] self.order_tree = ttk.Treeview(order_frame, columns=self.order_columns, show="headings", height=10) for col in self.order_columns: self.order_tree.heading(col, text=col) self.order_tree.column(col, anchor="center", width=100) self.order_tree.pack(fill="both", expand=True, padx=5, pady=5) vsb2 = ttk.Scrollbar(order_frame, orient="vertical", command=self.order_tree.yview) self.order_tree.configure(yscrollcommand=vsb2.set) vsb2.pack(side="right", fill="y") def update(self, account_info: dict, positions: list, orders: list): # === Update account info === acc_text = ( f"Balance: {account_info['balance']:.2f} | " f"Equity: {account_info['equity']:.2f} | " f"Profit: {account_info['profit']:.2f} | " f"Margin: {account_info['margin']:.2f} | " f"Free margin: {account_info['free_margin']:.5f} | " f"Margin level: {account_info['margin_level']:.2f}%" ) self.account_label.config(text=acc_text) # === Refresh positions === for row in self.position_tree.get_children(): self.position_tree.delete(row) for pos in positions: row = [pos.get(col, "") for col in self.position_columns] self.position_tree.insert("", "end", values=row) # === Refresh orders === for row in self.order_tree.get_children(): self.order_tree.delete(row) for order in orders: row = [] for col in self.order_columns: val = order.get(col, "") if isinstance(val, datetime): val = val.strftime("%Y-%m-%d %H:%M:%S") row.append(val) self.order_tree.insert("", "end", values=row) self.root.update() def run(self): self.root.mainloop()
Die obige Klasse erstellt zwei Tabellen, eine Tabelle für die Anzeige von Orders und die andere für Positionen. Im oberen Bereich der GUI zeigen wir zusätzlich Kontoinformationen an.
Innerhalb der Klasse TradeSimulator initialisieren wir diese Simulation ToolBox GUI im Klassenkonstruktor.
Innerhalb von trade_simulator.py
from toolbox_gui import SimToolboxGUI class TradeSimulator: def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): # ... other variables self.toolbox_gui = SimToolboxGUI() # Initialize the GUI
Für die Aktualisierung der in der GUI-Anwendung angezeigten Daten erstellen wir eine eigene Funktion.
class TradeSimulator: # ... other functions def run_toolbox_gui(self): """ Runs the simulator toolbox GUI. """ self.toolbox_gui.update(self.account_info, self.open_trades_container)
Nach dem Aufruf von Funktionen zur Überwachung und Regulierung von Positionen, Orders und des Kontos. Wir rufen die Funktion zum Aktualisieren der GUI-Anwendung auf.
while True: # constantly monitor trades and account metrics sim.monitor_account(verbose=False) sim.monitor_positions(verbose=False) sim.monitor_orders() sim.run_toolbox_gui() # Run the simulator toolbox GUI time.sleep(1) # sleep for one second
Lassen Sie uns noch einmal einige Positionen und Orders sowohl im MetaTrader 5 als auch im Python-Simulator eröffnen und dann die Ergebnisse in beiden beobachten.
Dateiname: simulator_test.py
if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") magic_number = 123456 slippage = 10 sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator m_trade = CTrade() # Initializing the CTrade class symbol = "EURUSD" m_trade.set_magicnumber(magic_number=magic_number) # sets the magic number of the CTrade class m_trade.set_deviation_in_points(deviation_points=slippage) # sets slippage m_trade.set_filling_type_by_symbol(symbol=symbol) #set filling type by the given symbol m_symbol = CSymbolInfo(mt5_instance=mt5) m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo # Open trades in a Simulator sim.monitor_account(verbose=False) if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5 print("failed to get recent ticks data") # Market Orders sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask()) sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid()) m_trade.buy(volume=0.1, symbol=symbol, price=m_symbol.ask()) m_trade.sell(volume=0.1, symbol=symbol, price=m_symbol.bid()) # Pending Orders expiry = datetime.now(tz=pytz.UTC) + timedelta(days=1) # expiration date for pending orders price_gap = 0.0005 # Buy Stop: place above current ask sim.buy_stop(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap, sl=0.0, tp=0.0, comment="Buy Stop Example", expiry_date=expiry, expiration_mode="daily") m_trade.buy_stop(volume=0.1, symbol=symbol, price=m_symbol.ask() + price_gap) # Buy Limit: place below current bid sim.buy_limit(volume=0.1, symbol=symbol, open_price=m_symbol.bid() - price_gap, sl=0.0, tp=0.0, comment="Buy Limit Example", expiry_date=expiry, expiration_mode="daily_excluding_stops") m_trade.buy_limit(volume=0.1, symbol=symbol, price=m_symbol.bid() - price_gap) # Sell Stop: place below current bid sim.sell_stop(volume=0.1, symbol=symbol, open_price=m_symbol.bid() - price_gap, sl=0.0, tp=0.0, comment="Sell Stop Example", expiry_date=expiry, expiration_mode="gtc") m_trade.sell_stop(volume=0.1, symbol=symbol, price=m_symbol.ask() - price_gap) # Sell Limit: place above current ask sim.sell_limit(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap, sl=0.0, tp=0.0, comment="Sell Limit Example", expiry_date=expiry, expiration_mode="gtc") m_trade.sell_limit(volume=0.1, symbol=symbol, price=m_symbol.bid() + price_gap) while True: # constantly monitor trades and account metrics sim.monitor_account() sim.monitor_pending_orders() sim.monitor_positions(verbose=False) sim.monitor_orders() sim.run_toolbox_gui() # Run the simulator toolbox GUI time.sleep(1) # sleep for one second
Ausgabe:

Großartig, unsere simulierten Handelsergebnisse liegen nicht sehr nahe an den tatsächlichen Handelsergebnissen, und sie sind auch nicht weiter davon entfernt, das ist ein großer Fortschritt.
Externe Verwaltung und Kontrolle von Positionen und Orders
Die Möglichkeit, Informationen über offene Positionen zu erhalten und diese außerhalb des Simulators zu kontrollieren, ist von entscheidender Bedeutung – das ist es, was den algorithmischen Handel ausmacht.
So erfordern beispielsweise viele Handelsstrategien die Kenntnis von zuvor eröffneten Positionen. Eine Handelsstrategie könnte beispielsweise verlangen, dass ein Roboter nur dann eine Kaufposition eröffnet, wenn keine Position in dieser Richtung und in diesem Instrument vorhanden ist.
Im Folgenden finden Sie eine Tabelle mit Funktionen, mit denen Sie auf alle Orders, Positionen und Deals außerhalb der Klasse TradeSimulator zugreifen können.
| Funktion | Rückgabe |
|---|---|
def get_positions(self) -> list: | Gibt alle offenen Positionen aus einem Container zurück. |
def get_orders(self) -> list: | Gibt alle offenen Orders aus einem Container zurück. |
def get_deals(self, start_time: datetime = None, end_time: datetime = None, from_db: bool = False) -> list: | Gibt alle Deals zurück, die zwischen einem bestimmten Zeitintervall, das durch die beiden Variablen (start_time und end_time) angegeben ist, ausgeführt wurden. Eine optionale Variable namens from_db hilft bei der Entscheidung zwischen der Auswahl von Deals, die vorübergehend im Speicher gespeichert sind, und der Auswahl aus der Datenbank. |
Beispiel für die Verwendung:
sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask()) sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid()) price_gap = 0.0005 # Buy Stop: place above current ask sim.buy_stop(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap) print("Positions total: ",len(sim.get_positions())) print("Orders total: ",len(sim.get_orders())) now = m_symbol.time(timezone=pytz.UTC) start_time = now - timedelta(minutes=5) end_time = now print("Deals total: ",len(sim.get_deals(start_time=start_time, end_time=end_time, from_db=False )))
Ausgabe:
(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy
Tester/simulator_test.py"
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 9, 59, 51, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14597, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 9, 59, 51, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14589, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Margin calculation mode: Calculation of profit and margin for Forex
Positions total: 2
Orders total: 1
Deals total: 2Bei der Auswahl der Deals sollten Sie die Uhrzeit des Symbols in UTC verwenden, die wir auch bei der Eröffnung von Positionen und Orders verwenden, und nicht die aktuelle Ortszeit, um Zeitdiskrepanzen zu vermeiden.
Diese Funktionen ermöglichen es uns dann, spezifische Bedingungen in unsere Handelsstrategien einzubringen.
(a): Prüfen, ob ein bestimmter Trade-Typ in einer Simulation existiert
Dies ist bei der Überwachung von Trades sehr üblich. Bei einigen Handelsstrategien wollen wir oft bestimmte Positionen und Orders nur dann eröffnen, wenn sie nicht existieren.
if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500") magic_number = 123456 slippage = 10 sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator symbol = "EURUSD" m_symbol = CSymbolInfo(mt5_instance=mt5) m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo def is_position_exists(type: str) -> bool: for pos in sim.get_positions(): if pos["magic"] == magic_number and pos["symbol"] == symbol and pos["type"] == type: return True # position exists return False while True: #imitating the OnTick function offered in MQL5 language sim.monitor_pending_orders() sim.monitor_positions(verbose=False) sim.monitor_account(verbose=False) sim.run_toolbox_gui() # Run the simulator toolbox GUI if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5 # print("failed to get recent ticks data") continue if not is_position_exists("buy"): # open a buy trade in a simulator if it doesn't exist sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask()) if not is_position_exists("sell"): # open a sell trade in a simulator if it doesn't exist sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid()) time.sleep(1) # sleep for one second
Ausgabe:
(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy
Tester/simulator_test.py"
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 10, 13, 18, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14565, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 10, 13, 18, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14557, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Es wurden nur zwei verschiedene Positionen eröffnet (Kauf- und Verkaufspositionen).
Dies ist eine vertraute Schnittstelle zu der in MQL5 angebotenen, die wir häufig verwenden, um zu prüfen, ob eine bestimmte Position existiert.
(b): Schließen einer bestimmten Position
def close_positions(type: str): for pos in sim.get_positions(): if pos["magic"] == magic_number and pos["symbol"] == symbol and pos["type"] == type: sim.position_close(pos)
Einige Strategien müssen möglicherweise bestimmte Trades bei einer bestimmten programmierten Bedingung schließen; in dieser Hinsicht ist die obige Funktion oder ein ähnlicher Ansatz sehr nützlich.
Eröffnen wir zwei Positionen (Kauf- und Verkaufspositionen) und schließen wir eine Kaufposition.
while True: sim.monitor_pending_orders() sim.monitor_positions(verbose=False) sim.monitor_account(verbose=False) sim.run_toolbox_gui() # Run the simulator toolbox GUI if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5 # print("failed to get recent ticks data") continue if not is_position_exists("buy"): # open a buy trade in a simulator if it doesn't exist sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask()) close_positions("buy") # close all buy positions if not is_position_exists("sell"): # open a sell trade in a simulator if it doesn't exist sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid()) time.sleep(1) # sleep for one second
Ausgabe:
(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy
Tester/simulator_test.py"
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14447, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade closed successfully: {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14447, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': '', 'direction': 'closed', 'reason': 'Take profit'}
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14439, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully: {'time': datetime.datetime(2025, 7, 31, 10, 50, 37, tzinfo=<UTC>), 'id': 3, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14446, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade closed successfully: {'time': datetime.datetime(2025, 7, 31, 10, 50, 37, tzinfo=<UTC>), 'id': 3, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14446, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': '', 'direction': 'closed', 'reason': 'Take profit'} Arbeiten mit Deals
Im MetaTrader 5 stellt ein Deal die tatsächliche Ausführung eines Handels dar – es ist das Ergebnis einer Order. Jeder Deal basiert auf einem bestimmten Auftrag, aber ein einzelner Auftrag kann mehrere Deals generieren (z. B. wenn der Auftrag in Teilen ausgeführt wird).
Deals werden erstellt, wenn.
- Eine Position eröffnet wird,
- Eine Position teilweise oder vollständig geschlossen wird,
- Oder eine Order (wie ein Limit- oder Stop-Order) ausgelöst und ausgeführt wird.
Mit anderen Worten: Sowohl Eröffnen wie Schließen werden als Deals erfasst.
Im Gegensatz zu Orders und Positionen, die vorübergehend geändert werden können, sind Deals unveränderlich und werden immer in der Handelshistorie gespeichert. Sie dienen als dauerhafte Aufzeichnung der getätigten Trades und können nicht geändert oder gelöscht werden.

Am Ende der beiden Funktionen _position_open und _position_close, die Positionen öffnen bzw. schließen, wird ein Deal zu einer Liste namens deals_container hinzugefügt, die sich im Konstruktor der Klasse befindet.
def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: trade_info = self.trade_info.copy() # ... other operations # ... # Append to open trades self.open_trades_container.append(trade_info) print("Trade opened successfully: ", trade_info) # Track deal self.deal_info.update(trade_info) self.deal_info["direction"] = "opened" self.deal_info["reason"] = "Expert" self.deals_container.append(self.deal_info.copy())
def position_close(self, selected_pos: dict) -> bool: # Update deal info deal_info = selected_pos.copy() deal_info["direction"] = "closed" # ... other operations deal_info["reason"] = "Unknown" # Unkown deal reason if the stoploss or takeprofit wasn't hit if selected_pos["type"] == "buy": if np.isclose(selected_pos["tp"], bid, digits): # check if the current bid price is almost equal to the takeprofit deal_info["reason"] = "Take profit" elif np.isclose(selected_pos["sl"], bid, digits): # check if the current bid price is almost equal to the stoploss deal_info["reason"] = "Stop loss" if selected_pos["type"] == "sell": if np.isclose(selected_pos["tp"], ask, digits): # check if the current ask price is almost equal to the takeprofit deal_info["reason"] = "Take profit" elif np.isclose(selected_pos["sl"], ask, digits): # check if the current ask price is almost equal to the stoploss deal_info["reason"] = "Stop loss" self.deals_container.append(deal_info.copy()) # add the deal to the deals container print("Trade closed successfully: ", deal_info)
Die Speicherung der vom Simulator geöffneten Deals in einer Liste/Array ist jedoch nicht ideal, da diese Informationen verloren gehen, sobald das Programm geschlossen wird. Wir speichern sie in einer SQLite3-Datenbank und machen ihre Aufzeichnungen dauerhaft, solange sie nicht geändert oder gelöscht werden, ähnlich wie es MetaTrader 5 macht.
def _create_deals_db(self, db_name: str): """ Creates a SQLite database to store trade history and account information. Args: db_name (str): The name of the database file. """ conn = sqlite3.connect(db_name) cursor = conn.cursor() # Create tables if they do not exist cursor.execute(''' CREATE TABLE IF NOT EXISTS closed_deals ( id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT, magic INTEGER, symbol TEXT, type TEXT, direction TEXT, volume REAL, price REAL, sl REAL, tp REAL, commission REAL, margin_required REAL, fee REAL, swap REAL, profit REAL, comment TEXT, reason TEXT ) ''') conn.commit() conn.close()
Die obige Funktion wird innerhalb der Klasse TradeSimulator im Konstruktor aufgerufen.
class TradeSimulator: def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"): # ... other variables # ... # Database for trade history self.sim_folder = "Simulations" os.makedirs(self.sim_folder, exist_ok=True) # Ensure the simulations path exists # Create the database file name self.history_db_name = os.path.join(self.sim_folder, self.simulator_name+".db") self._create_deals_db(self.history_db_name)
Nach der Erstellung einer Datenbank, die dem Namen des Simulators entspricht, der durch die Variable simulator_name angegeben wird, erstellt die Funktion _create_deals_db eine Tabelle mit dem Namen closed_deals, falls diese nicht existiert.
Wir brauchen auch eine Funktion zum Speichern jedes einzelnen Deals in der Datenbank.
def _save_deal(self, deal: dict, db_name: str): """ Saves a closed deal to the SQLite database. """ conn = sqlite3.connect(db_name) cursor = conn.cursor() cursor.execute(""" INSERT INTO closed_deals ( time, magic, symbol, type, direction, volume, price, sl, tp, commission, margin_required, fee, swap, profit, comment, reason ) VALUES ( :time, :magic, :symbol, :type, :direction, :volume, :price, :sl, :tp, :commission, :margin_required, :fee, :swap, :profit, :comment, :reason ); """, deal) conn.commit() conn.close()
Haben Sie bemerkt, dass wir der Datenbank keine Spalte namens id hinzufügen? Der Grund dafür ist, dass die Spalte id in der Datenbanktabelle auf AUTOINCREMENT gesetzt ist, um sicherzustellen, dass jedem Deal eine eindeutige ID in der gesamten Historie von 0 bis positiv unendlich zugewiesen wird.
Wir müssen alle Deals in den Funktionen, die für die Eröffnung und Schließung von Positionen zuständig sind, in der Datenbank speichern, nachdem wir sie in der Liste namens deals_container abgelegt haben.
Innerhalb der Funktion mit dem Namen position_close.
def position_close(self, selected_pos: dict) -> bool: # Update deal info deal_info = selected_pos.copy() deal_info["direction"] = "closed" #... #... print("Trade closed successfully: ", deal_info) # Save closed deal to database self._save_deal(deal_info, self.history_db_name)
Innerhalb der Funktion namens _open_position.
def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool: trade_info = self.trade_info.copy() #... #... #... self.deals_container.append(self.deal_info.copy()) # Log to database self._save_deal(self.deal_info, self.history_db_name) return True
Nachfolgend finden Sie die SQLite-Datenbank mit allen Deals der letzten Stunden und Tage.

Abschließende Überlegungen
Während ich diesen ersten Teil eines MetaTrader 5-Simulators implementiere, kann ich nicht umhin zu schätzen, wie ausgeklügelt der Strategietester des MetaTrader 5 ist. Im Hintergrund dieses Tools laufen viele Dinge ab, abgesehen von der reinen Ausführung von Trades.
An dieser Stelle fragen Sie sich vielleicht: Ist dieser Simulator notwendig? Weil wir einen Handelssimulator implementiert haben, der Trades auf einem Konto eröffnet, das wie ein echtes Konto aussieht, was sich nicht von dem unterscheidet, was die MetaTrader 5-Anwendung bei Verwendung des MetaTrader5-Python-Moduls tut?
Das Ziel dieses Artikels war es, die Dynamik eines Handelssimulators zu verstehen. Indem wir einige einfache Trades simulieren und sicherstellen, dass sie den in einem realen Konto eröffneten Trades sehr ähnlich sind, können wir sagen, dass wir uns unserem Ziel nähern.
Es ist auch fair zu sagen, dass dieser Simulator im Vergleich zum Strategietester der MetaTrader 5-Plattform bei weitem nicht vollständig/perfekt ist. Es gibt immer noch viele Dinge, die fehlen oder nicht richtig gemacht wurden. Es ist eine Herausforderung, den Überblick über alle Details zu behalten. Um ehrlich zu sein: Wenn Sie Gedanken und Meinungen haben, oder an dem Projekt mitarbeiten wollen, hier ist der Link zu einem Repository auf GitHub -> https://github.com/MegaJoctan/PyMetaTester.
Was kommt als Nächstes?
Im oben erwähnten Handelssimulator haben wir wichtige Informationen aus dem Markt extrahiert, wie z. B. den aktuellen Geld- und Briefkurs sowie andere wichtige Informationen über das ausgewählte Symbol. In den nächsten Artikeln werden wir verschiedene Möglichkeiten erörtern, wie wir Tickdaten extrahieren und diese Informationen in einer Schleife wiederholen können, um das historische Testverhalten des Strategietesters zu imitieren.
Bis zum nächsten Mal.
Tabelle der Anhänge
| Dateiname | Beschreibung und Verwendung |
|---|---|
| requirements.txt | Enthält alle in diesem Projekt verwendeten Python-Abhängigkeiten. |
| trade_simulator.py | Die Datei verfügt über die Klasse TradeSimulator, die den gesamten Handelssimulator enthält. |
| simulator_test.py | Ein Testskript/Playground-Skript zum Ausprobieren des beschriebenen Handelssimulators. |
| toolbox_gui.py | Enthält eine einfache, dem MetaTrader 5 ähnliche GUI-Anwendung zur Anzeige von Handels- und Kontostandsinformationen |
| Trade\SymbolInfo.py | Enthält die Klasse CSymbolInfo, die alle Informationen von MetaTrader 5 über ein bestimmtes Symbol liefert. |
| Trade\Trade.py | Enthält die Klasse CTrade, die Funktionen zum Eröffnen von Positionen und Orders im MetaTrader 5 unter Verwendung des Metatrader5-Python-Moduls bereitstellt. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/18971
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Vom Einsteiger zum Experten: Entwicklung einer Liquiditätsstrategie
Einführung in MQL5 (Teil 38): Beherrschung von API und WebRequest in MQL5 (XII)
Entwicklung des Price Action Analysis Toolkit (Teil 58): Modul zur Analyse der Spannenverengung und Reifegradklassifizierung
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
