Python-MetaTrader 5 Strategietester (Teil 03): MetaTrader 5-ähnliche Handelsoperationen – Handhabung und Verwaltung
Inhalt
- Einführung
- Die Methode order_send
- Die Klasse für die Handelsvalidierung
- Alle Prüfer innerhalb von order_send
- Die Klasse CTrade innerhalb eines Simulators
- Durchführung von Handelsaktionen in einem Simulator
- Verwaltung von Aufträgen und Positionen in einem Simulator
- Schlussfolgerung
Einführung
Im vorangegangenen Artikel haben wir in unserem Simulator eine ähnliche Syntax und ähnliche Funktionen implementiert, wie sie das Python-MetaTrader-Modul bietet. Mit ähnlichen Aufträgen, Deals, Positionen und Strukturen. In diesem Beitrag werden wir einen MetaTrader 5-ähnlichen Ansatz für die Handhabung solcher Strukturen (Handelsoperationen) implementieren.

Die Methode order_send
Alle Handelsaktionen für MetaTrader 5, wie z. B. das Platzieren von schwebenden Aufträgen, das Eröffnen von Kauf- oder Verkaufspositionen, das Ändern von Aufträgen und das Löschen von Aufträgen, werden durch den Aufruf einer einzigen Funktion als Code-Baustein erzielt.
In MQL5 heißt diese Funktion OrderSend, in Python-MetaTrader 5 heißt sie order_send.
In der Dokumentation heißt es dazu.
Die Methode order_send sendet eine Anfrage zur Durchführung einer Handelsoperation vom Terminal an den Handelsserver. Sie ist ähnlich wie OrderSend.
order_send(
request // request structure
); Sie benötigt einen einzigen Parameter namens request. Die Struktur vom Typ MqlTradeRequest beschreibt eine erforderliche Handelsaktion.
In der folgenden Tabelle sind die Felder aufgeführt, die die „Anforderungsstruktur“ enthalten muss.
| Feld | Beschreibung |
|---|---|
| action | Art der Handelsoperation. Der Wert kann einer der Werte aus der Enumeration TRADE_REQUEST_ACTIONS sein |
| magic | EA-ID. Ermöglicht die Organisation der analytischen Bearbeitung von Handelsaufträgen. Jeder EA kann beim Senden einer Handelsanfrage eine eindeutige ID festlegen |
| order | Ticket des Auftrags. Erforderlich für die Änderung schwebender Aufträge |
| symbol | Der Name des Handelsinstruments, für das der Auftrag erteilt wird. Nicht erforderlich bei der Änderung von Aufträgen und der Schließung von Positionen |
| volume | Angefordertes Volumen eines Deals in Losen. Das tatsächliche Volumen beim Abschluss eines Deals hängt von der Art der Auftragsausführung ab. |
| price | Preis, zu dem ein Auftrag ausgeführt werden soll. Der Preis wird für ein Instrument bei Marktaufträgen der Art „Marktausführung“ (SYMBOL_TRADE_EXECUTION_MARKET) mit der Art TRADE_ACTION_DEAL nicht festgelegt |
| stoplimit | Ein preisabhängiger Limitauftrag wird erteilt, wenn der Preis den Wert „price“ erreicht (diese Option ist obligatorisch). Der schwebende Auftrag wird erst zu diesem Zeitpunkt an das Handelssystem weitergeleitet. |
| sl | Ein Kurs, bei dem ein Stop-Loss-Auftrag aktiviert wird, wenn sich der Kurs in eine ungünstige Richtung bewegt |
| tp | Ein Kurs, bei dem ein Take-Profit-Auftrag aktiviert wird, wenn sich der Kurs in eine günstige Richtung bewegt |
| deviation | Maximal akzeptable Abweichung vom geforderten Preis, angegeben in Punkten |
| type | Typ des Auftrags. Der Wert kann einer der Werte aus der Enumeration ORDER_TYPE sein |
| type_filling | Art der Auftragsabwicklung. Der Wert kann einer der Werte aus der Enumeration ORDER_TYPE_FILLING sein. |
| type_time | Auftragsart mit einem Ablauf. Der Wert kann einer der Werte von ORDER_TYPE_TIME sein. |
| expiration | Ablaufzeit für schwebende Aufträge (für Aufträge vom Typ TIME_SPECIFIED ) |
| comment | Kommentar eines Auftrags |
| position | Ticketnummer der Position. Geben Sie diese Option an, wenn Sie eine Position ändern oder schließen, um sie eindeutig zu identifizieren. In der Regel ist es dasselbe wie das Ticket des Auftrags, der die Position eröffnet hat. |
| position_by | Ticket für die entgegengesetzte Position. Sie wird verwendet, wenn eine Position eröffnet wird, um dadurch eine entgegengesetzte Position (mit demselben Symbol) zu schließen. |
Wir benötigen eine ähnliche Funktion in unserer Klasse.
def order_send(self, request: dict): """ Sends a request to perform a trading operation from the terminal to the trade server. The function is similar to OrderSend in MQL5. """ if not self.IS_TESTER: result = self.mt5_instance.order_send(request) if result is None or result.retcode != self.mt5_instance.TRADE_RETCODE_DONE: self.__GetLogger().warning(f"MT5 failed: {self.mt5_instance.last_error()}") return None return result
Im vorangegangenen Artikel haben wir der Simulatorklasse den Modus Strategietester hinzugefügt, d. h., wenn die Variable IS_TESTER = true ist. Wenn sich der Simulator nicht in diesem Modus befindet, greift er auf Informationen des MetaTrader 5-Clients zurück, und öffnet und verwaltet alle Handelsoperationen dort.
Das obige Code-Fragment sendet eine Order-Anfrage an MetaTrader 5, wenn sich der Nutzer nicht im Tester-Modus befindet.
Andernfalls extrahieren wir die Anmeldedaten der Anfrage.
action = request.get("action") order_type = request.get("type") symbol = request.get("symbol") volume = float(request.get("volume", 0)) price = float(request.get("price", 0)) sl = float(request.get("sl", 0)) tp = float(request.get("tp", 0)) ticket = int(request.get("ticket", -1)) ticks_info = self.tick_cache[symbol] now = utils.ensure_utc(ticks_info.time) ts = int(now.timestamp()) msc = int(now.timestamp() * 1000)
Wir müssen alle Vorgänge innerhalb dieser Funktion manuell ausführen, einschließlich des Schreibens (Eröffnung und Schließung von Positionen) und des Übertragens der Deals in einen Container (letztlich übernehmen wir damit die Arbeit von MetaTrader 5).
I: Schwebende Aufträge platzieren
In einem Simulator ist ein schwebender Auftrag nichts anderes als eine Information über einen Auftrag, die in einem temporären Auftrags-Container-Array (self.__orders_container__) gespeichert ist.
if action == self.mt5_instance.TRADE_ACTION_PENDING: order_ticket = self.__generate_order_ticket() order = self.TradeOrder( ticket=order_ticket, time_setup=ts, time_setup_msc=msc, time_done=0, time_done_msc=0, time_expiration=request.get("expiration", 0), type=order_type, type_time=request.get("type_time", 0), type_filling=request.get("type_filling", 0), state=self.mt5_instance.ORDER_STATE_PLACED, magic=request.get("magic", 0), position_id=0, position_by_id=0, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume_initial=volume, volume_current=volume, price_open=price, sl=sl, tp=tp, price_current=price, price_stoplimit=request.get("price_stoplimit", 0), symbol=symbol, comment=request.get("comment", ""), external_id="", )
Nachdem ein Auftrag erstellt wurde, wird er dem Container-Array für die Aufträge hinzugefügt und auch im Array für den Auftragsverlauf (Container) protokolliert.
self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, }
II: Positionen eröffnen
In MetaTrader 5 sind Positionen Verträge, die für ein Finanzinstrument gekauft oder verkauft werden. Eine Long-Position (Long) entsteht durch den Kauf in Erwartung eines Preisanstiegs, eine Short-Position (Short) durch den Verkauf eines Vermögenswerts in Erwartung eines zukünftigen Preisrückgangs.
In einem Simulator ist eine Position ein Bündel von „positionsähnlichen“ Informationen, die in einem Container gespeichert sind.
if action == self.mt5_instance.TRADE_ACTION_DEAL: position_ticket = self.__generate_position_ticket() order_ticket = self.__generate_order_ticket() deal_ticket = self.__generate_deal_ticket() position = self.TradePosition( ticket=position_ticket, time=ts, time_msc=msc, time_update=ts, time_update_msc=msc, type=order_type, magic=request.get("magic", 0), identifier=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price_open=price, sl=sl, tp=tp, price_current=price, swap=0, profit=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__positions_container__.append(position)
Die Vorgänge der Eröffnung und Schließung von Positionen haben die Aktion TRADE_ACTION_DEAL. Das Ergebnis eines solchen Vorgangs kann als Deal bezeichnet werden, daher müssen wir einen solchen Datensatz in einem Container der Deals protokollieren.
self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=order_ticket, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_IN, magic=request.get("magic", 0), position_id=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, "order": order_ticket, "position": position_ticket, }
Ein Deal ist einfach eine Buchung (in der Vergangenheit) der Eröffnung und Schließung von Positionen im MetaTrader 5 Terminal.

III: Positionen schließen
Der Antrag auf Schließung einer Position ist dem Antrag auf Eröffnung einer neuen Position sehr ähnlich. Beides sind Deals, aber mit unterschiedlichen Eröffnungen.
Um einen Antrag auf Schließung einer Position zu erkennen, prüfen wir, ob er einen Positionsschlüssel in seinem Antrag hat (das Ticket einer bestehenden Position).
if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, }
Wir können jedoch nicht jeden Antrag auf Schließung einer Position blindlings akzeptieren, sondern müssen ihn prüfen.
Bevor wir einen Deal abschließen und eine Position aus dem Container entfernen, müssen wir zwei wichtige Details überprüfen.
- Im MetaTrader 5 werden Kaufpositionen zum Geldkurs geschlossen, während Verkaufspositionen zum Briefkurs geschlossen werden.
- Wir prüfen, ob eine in der Anfrage angegebene Auftragsart (Positionstyp) das Gegenteil eines bestehenden Auftrags ist, d. h., wenn eine Anfrage für einen bestehenden Kaufauftrag ORDER_TYPE_BUY gesendet wird, sollte er für die Schließung infrage kommen, wenn er ORDER_TYPE_SELL erhält.
if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate position close request if pos.type == order_type: self.__GetLogger().critical("Failed to close an order. Order type must be the opposite") return None if order_type == self.mt5_instance.ORDER_TYPE_BUY: # For a sell order/position if not TradeValidators.price_equal(a=price, b=ticks_info.ask, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_SELL. Price {price} is not equal to bid {ticks_info.bid}") return None elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # For a buy order/position if not TradeValidators.price_equal(a=price, b=ticks_info.bid, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_BUY. Price {price} is not equal to bid {ticks_info.bid}") return None self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, }
IV: Ändern von Positionen
Um eine Position zu ändern, konzentrieren wir uns nur auf zwei Details. Die Werte von Stop-Loss und Take-Profit.
elif action == self.mt5_instance.TRADE_ACTION_SLTP: ticket = request.get("position", -1) pos = next((p for p in self.__positions_container__ if p.ticket == ticket), None) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # --- Correct reference prices --- entry_price = pos.price_open market_price = ticks_info.bid if pos.type == self.mt5_instance.POSITION_TYPE_BUY else ticks_info.ask # --- Validate SL / TP relative to ENTRY --- if sl > 0: if not trade_validators.is_valid_sl(entry=entry_price, sl=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_tp(entry=entry_price, tp=tp, order_type=pos.type): return None # --- Validate freeze level against MARKET --- if sl > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=tp, order_type=pos.type): return None # --- APPLY MODIFICATION --- idx = self.__positions_container__.index(pos) updated_pos = pos._replace( sl=sl, tp=tp, time_update=ts, time_update_msc=msc ) self.__positions_container__[idx] = updated_pos return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
V: Löschen von schwebenden Aufträgen
Dies ist ein einfaches Verfahren, um einen Auftrag aus seinem Container-Array zu entfernen. Eine Validierungen ist nicht erforderlich.
if action == self.mt5_instance.TRADE_ACTION_REMOVE: ticket = request.get("order", -1) self.__orders_container__ = [ o for o in self.__orders_container__ if o.ticket != ticket ] return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
VI: Ändern von schwebenden Aufträgen
Um einen schwebenden Auftrag zu ändern, müssen wir uns auf fünf entscheidende Details konzentrieren. Eröffnungskurs, Stop-Loss, Take-Profit, Ablaufzeit und Stop-Limit der Order.
elif action == self.mt5_instance.TRADE_ACTION_SLTP: ticket = request.get("position", -1) pos = next((p for p in self.__positions_container__ if p.ticket == ticket), None) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # --- Correct reference prices --- entry_price = pos.price_open market_price = ticks_info.bid if pos.type == self.mt5_instance.POSITION_TYPE_BUY else ticks_info.ask # --- Validate SL / TP relative to ENTRY --- if sl > 0: if not trade_validators.is_valid_sl(entry=entry_price, sl=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_tp(entry=entry_price, tp=tp, order_type=pos.type): return None # --- Validate freeze level against MARKET --- if sl > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=tp, order_type=pos.type): return None # --- APPLY MODIFICATION --- idx = self.__positions_container__.index(pos) updated_pos = pos._replace( sl=sl, tp=tp, time_update=ts, time_update_msc=msc ) self.__positions_container__[idx] = updated_pos return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
All diese Maßnahmen funktionieren jedoch nicht, wenn es keine zentrale(n) Methode(n) gibt, um die Anmeldeinformationen der Anfrage überhaupt zu validieren.
Im ersten Simulator, den wir in dieser Artikelserie implementiert haben, hatten wir eine unorganisierte Art und Weise, die Berechtigungsnachweise der Aufträge zu validieren. Dieses Mal verbessern wir sie, indem wir eine eigene Klasse für diese Aufgabe implementieren.
Die Klasse für die Handelsvalidierung
Wie wir wissen, akzeptiert MetaTrader 5 nicht alle übergebenen Anfragen. Es prüft auf ungültige Anmeldedaten und gibt eine Fehlermeldung aus, wenn dies der Fall ist, und lehnt solche Aufträge ab.
Diese Berechtigungsnachweise werden entsprechend der Spezifikation eines bestimmten Instruments, eines Kontotyps, der Anforderungen des Brokers und manchmal auch der MetaTrader 5 Kundenlimits validiert.
I: Prüfung der richtigen Losgröße
Für eine Losgröße, die von MetaTrader 5 akzeptiert wird:
- Sie muss größer sein als die minimal zulässige Losgröße (Volumen) für ein bestimmtes Instrument (Symbol).
- Sie muss kleiner sein als die maximal zulässige Losgröße für ein bestimmtes Instrument.
- Sie muss ein Vielfaches der Schrittweite des Volumens sein.
Innerhalb von validators.py
class TradeValidators: def __init__(self, symbol_info: namedtuple, ticks_info: any, logger: any, mt5_instance: mt5=mt5): self.symbol_info = symbol_info self.ticks_info = ticks_info self.logger = logger self.mt5_instance = mt5_instance def is_valid_lotsize(self, lotsize: float) -> bool: # Validate lotsize if lotsize < self.symbol_info.volume_min: # check if the received lotsize is smaller than minimum accepted lot of a symbol self.logger.info(f"Trade validation failed: lotsize ({lotsize}) is less than minimum allowed ({self.symbol_info.volume_min})") return False if lotsize > self.symbol_info.volume_max: # check if the received lotsize is greater than the maximum accepted lot self.logger.info(f"Trade validation failed: lotsize ({lotsize}) is greater than maximum allowed ({self.symbol_info.volume_max})") return False step_count = lotsize / self.symbol_info.volume_step if abs(step_count - round(step_count)) > 1e-7: # check if the stoploss is a multiple of the step size self.logger.info(f"Trade validation failed: lotsize ({lotsize}) must be a multiple of step size ({self.symbol_info.volume_step})") return False return True
II: Sicherstellen, dass genügend Geld für eine neue Stelle vorhanden ist
Das MetaTrader 5-Terminal prüft, ob genügend freie Marge auf dem Konto vorhanden ist, um eine neue Position zu eröffnen.
Nachfolgend finden Sie eine ähnliche Funktion für die Aufgabe.
def is_there_enough_money(self, margin_required: float, free_margin: float) -> bool: if margin_required < 0: self.logger.info("Trade validation failed: Cannot calculate margin requirements") return False # Check free margin if margin_required > free_margin: self.logger.info(f'Trade validation failed: Not enough money to open trade. ' f'Required: {margin_required:.2f}, ' f'Free margin: {free_margin:.2f}') return False return True
III: Prüfen, ob ein gültiger Eintrag vorhanden ist
Bei einer Kaufposition muss der Preis gleich dem Briefkurs sein, bei einer Verkaufsposition muss der Preis gleich dem Geldkurs sein. Diese Prüfung gilt nur für Positionen.
def is_valid_entry(self, price: float, order_type: int) -> bool: eps = pow(10, -self.symbol_info.digits) if order_type == self.mt5_instance.ORDER_TYPE_BUY: # BUY if not self.price_equal(a=price, b=self.ticks_info.ask, eps=eps): self.logger.info(f"Trade validation failed: Buy price {price} != ask {self.ticks_info.ask}") return False elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # SELL if not self.price_equal(a=price, b=self.ticks_info.bid, eps=eps): self.logger.info(f"Trade validation failed: Sell price {price} != bid {self.ticks_info.bid}") return False else: self.logger.error("Unknown MetaTrader 5 position type") return False return True
IV: Sicherstellen, dass die Stop-Loss- und Take-Profit-Werte nicht zu nahe am Markt liegen
Alle Symbole verfügen über einen kleinen Schwellenwert, der den Mindestabstand angibt, in dem Stop-Loss- und Take-Profit-Werte vom Markt platziert werden müssen.
Dieser Schwellenwert wird SYMBOL_TRADE_STOPS_LEVEL genannt.
def is_valid_stops_level(self, entry: float, stop_price: float, stops_type: str='') -> bool: point = self.symbol_info.point stop_level = self.symbol_info.trade_stops_level * point distance = abs(entry-stop_price) if stop_price <= 0: return True if distance < stop_level: self.logger.info(f"{'Either SL or TP' if stops_type=='' else stops_type} is too close to the market. Min allowed distance = {stop_level}") return False return True
V: Prüfen auf gültige Stop-Loss- und Take-Profit-Werte
Bei einem Kaufauftrag muss ein Stop-Loss unter dem Einstiegskurs liegen, während der Take-Profit über dem Einstiegskurs liegen muss.
Bei einem Verkaufsauftrag muss der Take-Profit-Wert unter dem Einstiegskurs liegen, während der Stop-Loss-Wert über dem Einstiegskurs liegen muss.
def is_valid_sl(self, entry: float, sl: float, order_type: int) -> bool: if not self.is_valid_stops_level(entry, sl, "Stoploss"): # check for stops levels return False if sl > 0: if order_type in self.BUY_ACTIONS: # buy action if sl >= entry: self.logger.info(f"Trade validation failed: Buy-based order's stop loss ({sl}) must be below order opening price ({entry})") return False elif order_type in self.SELL_ACTIONS: # sell action if sl <= entry: self.logger.info(f"Trade validation failed: Sell-based order's stop loss ({sl}) must be above order opening price ({entry})") return False else: self.logger.error("Unknown MetaTrader 5 order type") return False return True def is_valid_tp(self, entry: float, tp: float, order_type: int) -> bool: if not self.is_valid_stops_level(entry, tp, "Takeprofit"): # check for stops and freeze levels return False if tp > 0: if order_type in self.BUY_ACTIONS: # buy position if tp <= entry: self.logger.info(f"Trade validation failed: {self.ORDER_TYPES_MAP[order_type]} take profit ({tp}) must be above order opening price ({entry})") return False elif order_type in self.SELL_ACTIONS: # sell position if tp >= entry: self.logger.info(f"Trade validation failed: {self.ORDER_TYPES_MAP[order_type]} take profit ({tp}) must be below order opening price ({entry})") return False else: self.logger.error("Unknown MetaTrader 5 order type") return False return True
VI: Eine Prüfung, um sicherzustellen, dass die maximale Losgröße für ein Instrument nicht erreicht wird
Bei einigen Symbolen gibt es bei einigen Brokern eine Obergrenze für die Gesamtgröße der Lots in einzelnen offenen Aufträgen und Positionen.
def is_symbol_volume_reached(self, symbol_volume: float, volume_limit: float) -> bool: """Checks if the maximum allowed volume is reached for a particular instrument Returns: bool: True if the condition is reached and False when it is not. """ if symbol_volume >= volume_limit and volume_limit > 0: self.logger.critical(f"Symbol Volume limit of {volume_limit} is reached!") return True return False
VII: Eine Überprüfung, um sicherzustellen, dass die maximale Anzahl von Aufträgen nicht erreicht wird
Bei einigen Konten ist die Anzahl der gleichzeitig offenen schwebenden Aufträge begrenzt. Wir müssen dies überprüfen, um sicherzustellen, dass wir nicht gegen die Regeln eines Demokontos verstoßen, genauso wie die MetaTrader-5-Plattform uns nicht erlauben würde, gegen die Regeln eines Echtgeldkontos zu verstoßen.
def is_max_orders_reached(self, open_orders: int, ac_limit_orders: int) -> bool: """Checks whether the maximum number of orders for the account is reached Args: open_orders (int): The number of opened orders ac_limit_orders (int): Maximum number of orders allowed for the account Returns: bool: True if the threshold is reached, otherwise, it returns false. """ if open_orders >= ac_limit_orders and ac_limit_orders > 0: self.logger.critical(f"Pending Orders limit of {ac_limit_orders} is reached!") return True return False
VIII: Prüfen des „Freeze Levels“
| Art der Order/Position | Aktivierungspreis | Prüfung |
|---|---|---|
| Buy Limit order | Ask | Ask-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL |
| Buy Stop order | Ask | OpenPrice-Ask >= SYMBOL_TRADE_FREEZE_LEVEL |
| Sell Limit order | Bid | OpenPrice-Bid >= SYMBOL_TRADE_FREEZE_LEVEL |
| Sell Stop order | Bid | Bid-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL |
| Buy position | Bid | TakeProfit-Bid >= SYMBOL_TRADE_FREEZE_LEVEL Bid-StopLoss >= SYMBOL_TRADE_FREEZE_LEVEL |
| Sell position | Ask | Ask-TakeProfit >= SYMBOL_TRADE_FREEZE_LEVEL StopLoss-Ask >= SYMBOL_TRADE_FREEZE_LEVEL |
def is_valid_freeze_level(self, entry: float, stop_price: float, order_type: int) -> bool: """ Check SYMBOL_TRADE_FREEZE_LEVEL for pending orders and open positions. """ freeze_level = self.symbol_info.trade_freeze_level if freeze_level <= 0: return True # No freeze restriction point = self.symbol_info.point freeze_distance = freeze_level * point bid = self.ticks_info.bid ask = self.ticks_info.ask def log_fail(msg: str, dist: float): self.logger.info( f"{msg} | distance={dist/point:.1f} pts < " f"freeze_level={freeze_level} pts" ) # ---------------- Pending Orders ---------------- if order_type == self.mt5_instance.ORDER_TYPE_BUY_LIMIT: dist = ask - entry if dist < freeze_distance: log_fail("BuyLimit cannot be modified: Ask - OpenPrice", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_SELL_LIMIT: dist = entry - bid if dist < freeze_distance: log_fail("SellLimit cannot be modified: OpenPrice - Bid", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_BUY_STOP: dist = entry - ask if dist < freeze_distance: log_fail("BuyStop cannot be modified: OpenPrice - Ask", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_SELL_STOP: dist = bid - entry if dist < freeze_distance: log_fail("SellStop cannot be modified: Bid - OpenPrice", dist) return False return True # ---------------- Open Positions (SL / TP modification) ---------------- # Buy position if order_type == self.mt5_instance.ORDER_TYPE_BUY: if stop_price <= 0: return True if stop_price < entry: # StopLoss dist = bid - stop_price if dist < freeze_distance: log_fail("Buy position SL cannot be modified: Bid - SL", dist) return False else: # TakeProfit dist = stop_price - bid if dist < freeze_distance: log_fail("Buy position TP cannot be modified: TP - Bid", dist) return False return True # Sell position if order_type == self.mt5_instance.ORDER_TYPE_SELL: if stop_price <= 0: return True if stop_price > entry: # StopLoss dist = stop_price - ask if dist < freeze_distance: log_fail("Sell position SL cannot be modified: SL - Ask", dist) return False else: # TakeProfit dist = ask - stop_price if dist < freeze_distance: log_fail("Sell position TP cannot be modified: Ask - TP", dist) return False return True self.logger.error("Unknown MetaTrader 5 order type") return False
Alle validierenden Funktionen innerhalb von order_send (TL;DR)
Mit all diesen Funktionen in einer Klasse namens TradeValidators in der Datei validators.py, die auf die Funktion order_send angewendet wird, sieht das Ganze wie folgt aus:
def order_send(self, request: dict): """ Sends a request to perform a trading operation from the terminal to the trade server. The function is similar to OrderSend in MQL5. """ # ----------------------------------------------------- if not self.IS_TESTER: result = self.mt5_instance.order_send(request) if result is None or result.retcode != self.mt5_instance.TRADE_RETCODE_DONE: self.__GetLogger().warning(f"MT5 failed: {self.mt5_instance.last_error()}") return None return result # -------------------- Extract request ----------------------------- action = request.get("action") order_type = request.get("type") symbol = request.get("symbol") volume = float(request.get("volume", 0)) price = float(request.get("price", 0)) sl = float(request.get("sl", 0)) tp = float(request.get("tp", 0)) ticket = int(request.get("ticket", -1)) ticks_info = self.tick_cache[symbol] symbol_info = self.symbol_info(symbol) ac_info = self.account_info() now = ticks_info.time ts = int(now) msc = int(now * 1000) if order_type not in self.ORDER_TYPES: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} trade_validators = TradeValidators(symbol_info=symbol_info, ticks_info=ticks_info, logger=self.__GetLogger(), mt5_instance=self.mt5_instance) # -------------------- REMOVE pending order ------------------------ if action == self.mt5_instance.TRADE_ACTION_REMOVE: ticket = request.get("order", -1) self.__orders_container__ = [ o for o in self.__orders_container__ if o.ticket != ticket ] return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} # --------------------- PENDING order -------------------------- if action == self.mt5_instance.TRADE_ACTION_PENDING: if trade_validators.is_max_orders_reached(open_orders=len(self.__orders_container__), ac_limit_orders=ac_info.limit_orders): return None if not trade_validators.is_valid_sl(entry=price, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=price, tp=tp, order_type=order_type): return None total_volume = sum([pos.volume for pos in self.__positions_container__]) + sum([order.volume for order in self.__orders_container__]) if trade_validators.is_symbol_volume_reached(symbol_volume=total_volume, volume_limit=symbol_info.volume_limit): return None order_ticket = self.__generate_order_ticket() order = self.TradeOrder( ticket=order_ticket, time_setup=ts, time_setup_msc=msc, time_done=0, time_done_msc=0, time_expiration=request.get("expiration", 0), type=order_type, type_time=request.get("type_time", 0), type_filling=request.get("type_filling", 0), state=self.mt5_instance.ORDER_STATE_PLACED, magic=request.get("magic", 0), position_id=0, position_by_id=0, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume_initial=volume, volume_current=volume, price_open=price, sl=sl, tp=tp, price_current=price, price_stoplimit=request.get("price_stoplimit", 0), symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, } # ------------------ MARKET DEAL (open or close) ------------------ if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate position close request if pos.type == order_type: self.__GetLogger().critical("Failed to close an order. Order type must be the opposite") return None if order_type == self.mt5_instance.ORDER_TYPE_BUY: # For a sell order/position if not TradeValidators.price_equal(a=price, b=ticks_info.ask, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_SELL. Price {price} is not equal to bid {ticks_info.bid}") return None elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # For a buy order/position if not TradeValidators.price_equal(a=price, b=ticks_info.bid, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_BUY. Price {price} is not equal to bid {ticks_info.bid}") return None self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, } # ---------- OPEN POSITION ---------- # validate new stops if not trade_validators.is_valid_sl(entry=price, sl=sl, order_type=order_type): return None if not trade_validators.is_valid_tp(entry=price, tp=tp, order_type=order_type): return None # validate the lotsize if not trade_validators.is_valid_lotsize(lotsize=volume): return None total_volume = sum([pos.volume for pos in self.__positions_container__]) + sum([order.volume for order in self.__orders_container__]) if trade_validators.is_symbol_volume_reached(symbol_volume=total_volume, volume_limit=symbol_info.volume_limit): return None if not trade_validators.is_there_enough_money(margin_required=self.order_calc_margin(order_type=order_type, symbol=symbol, volume=volume, price=price), free_margin=ac_info.margin_free): return None position_ticket = self.__generate_position_ticket() order_ticket = self.__generate_order_ticket() deal_ticket = self.__generate_deal_ticket() position = self.TradePosition( ticket=position_ticket, time=ts, time_msc=msc, time_update=ts, time_update_msc=msc, type=order_type, magic=request.get("magic", 0), identifier=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price_open=price, sl=sl, tp=tp, price_current=price, swap=0, profit=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__positions_container__.append(position) self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=order_ticket, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_IN, magic=request.get("magic", 0), position_id=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, "order": order_ticket, "position": position_ticket, } elif action == self.mt5_instance.TRADE_ACTION_MODIFY: # Modifying pending orders ticket = request.get("order", -1) order = next( (o for o in self.__orders_container__ if o.ticket == ticket), None, ) if not order: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate new stops if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=tp, order_type=order_type): return None # Modify ONLY allowed fields order.price_open = price order.sl = sl order.tp = tp order.time_expiration = request.get("expiration", order.time_expiration) order.price_stoplimit = request.get("price_stoplimit", order.price_stoplimit) return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} elif action == self.mt5_instance.TRADE_ACTION_SLTP: # Modifying an open position ticket = request.get("position", -1) pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # Check for valid stoplosses and TPs ensuring they are not too close to the market if pos.type == self.mt5_instance.ORDER_TYPE_BUY: if not trade_validators.is_valid_sl(entry=ticks_info.bid, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=ticks_info.bid, tp=tp, order_type=order_type): return None elif pos.type == self.mt5_instance.ORDER_TYPE_SELL: if not trade_validators.is_valid_sl(entry=ticks_info.ask, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=ticks_info.ask, tp=tp, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None pos.sl = sl pos.tp = tp pos.time_update = ts pos.time_update_msc = msc return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} return { "retcode": self.mt5_instance.TRADE_RETCODE_INVALID, "comment": "Unsupported trade action", }
Die Klasse CTrade innerhalb eines Simulators
Genau wie MQL5 ist das Python-MetaTrader 5-Modul ein Low-Level-Modul, das uns die Kommunikation mit dem MetaTrader 5-Terminal ermöglicht. Wie wir soeben gesehen haben, braucht es mehr als nur einen Antrag auf Eröffnung von Positionen und Aufträgen (ein langwieriger und mühsamer Prozess).
In MQL5 haben wir die sogenannten Handelsklassen, die uns eine einfache Schnittstelle für die Eröffnung und Verwaltung von Geschäften bieten. In Python haben wir eine ähnliche Klasse erstellt. Passen wir jetzt die Klasse CTrade an die Bedürfnisse unseres Simulators an.
import MetaTrader5 as mt5 from datetime import datetime, timezone import config class CTrade: def __init__(self, simulator, magic_number: int, filling_type_symbol: str, deviation_points: int): self.simulator = simulator self.mt5_instance = simulator.mt5_instance self.magic_number = magic_number self.deviation_points = deviation_points self.filling_type = self._get_type_filling(filling_type_symbol) if self.filling_type == -1: print("Failed to initialize the class, Invalid filling type. Check your symbol") return
Wir geben unserer Klasse eine Simulatorinstanz, um einige Methoden daraus zu extrahieren, anstatt sie direkt vom MetaTrader 5 zu übernehmen. Das hilft unserer Klasse, überschriebene Methoden zu verwenden, die im vorherigen Artikel besprochen wurden.
Die Klasse CTrade verfügt über zahlreiche Methoden, die alle auf einer Methode namens order_send basieren, die mit dem Python-MetaTrader 5-Modul geliefert wird.
In der gesamten Klasse werden keine Methoden aus dem Python-MetaTrader 5-Modul aufgerufen, sondern überschriebene Methoden aus einer Simulator-Klasse.
class CTrade: def __init__(self, simulator, magic_number: int, filling_type_symbol: str, deviation_points: int): self.simulator = simulator self.mt5_instance = simulator.mt5_instance self.magic_number = magic_number self.deviation_points = deviation_points self.filling_type = self._get_type_filling(filling_type_symbol) if self.filling_type == -1: print("Failed to initialize the class, Invalid filling type. Check your symbol") return def _get_type_filling(self, symbol): symbol_info = self.simulator.symbol_info(symbol) if symbol_info is None: print(f"Failed to get symbol info for {symbol}") filling_map = { 1: self.mt5_instance.ORDER_FILLING_FOK, 2: self.mt5_instance.ORDER_FILLING_IOC, 4: self.mt5_instance.ORDER_FILLING_BOC, 8: self.mt5_instance.ORDER_FILLING_RETURN } return filling_map.get(symbol_info.filling_mode, f"Unknown Filling type") def __GetLogger(self): if self.simulator.IS_TESTER: return config.tester_logger return config.simulator_logger def position_open(self, symbol: str, volume: float, order_type: int, price: float, sl: float=0.0, tp: float=0.0, comment: str="") -> bool: """ Open a market position (instant execution). Executes either a buy or sell order at the current market price. This is for immediate position opening, not pending orders. Args: symbol: Trading symbol (e.g., "EURUSD", "GBPUSD") volume: Trade volume in lots (e.g., 0.1 for micro lot) order_type: Trade direction (either ORDER_TYPE_BUY or ORDER_TYPE_SELL) price: Execution price. For market orders, this should be the current: - Ask price for BUY orders - Bid price for SELL orders sl: Stop loss price (set to 0.0 to disable) tp: Take profit price (set to 0.0 to disable) comment: Optional order comment (max 31 characters, will be truncated automatically) Returns: bool: True if position was opened successfully, False otherwise """ request = { "action": self.mt5_instance.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "deviation": self.deviation_points, "magic": self.magic_number, "comment": comment, "type_time": self.mt5_instance.ORDER_TIME_GTC, "type_filling": self.filling_type, } if sl > 0.0: request["sl"] = sl if tp > 0.0: request["tp"] = tp if self.simulator.order_send(request) is None: return False self.__GetLogger().info(f"Position Opened successfully!") return True def order_open(self, symbol: str, volume: float, order_type: int, price: float, sl: float = 0.0, tp: float = 0.0, type_time: int = mt5.ORDER_TIME_GTC, expiration: datetime = None, comment: str = "") -> bool: """ Opens a pending order with full control over order parameters. Args: symbol: Trading symbol (e.g., "EURUSD") volume: Order volume in lots order_type: Order type (ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_STOP, etc.) price: Activation price for pending order sl: Stop loss price (0 to disable) tp: Take profit price (0 to disable) type_time: Order expiration type (default: ORDER_TIME_GTC). Possible values: - ORDER_TIME_GTC (Good-Til-Canceled) - ORDER_TIME_DAY (Good for current day) - ORDER_TIME_SPECIFIED (expires at specific datetime) - ORDER_TIME_SPECIFIED_DAY (expires at end of specified day) expiration: Expiration datetime (required for ORDER_TIME_SPECIFIED types) comment: Optional order comment (max 31 characters) Returns: bool: True if order was placed successfully, False otherwise """ # Validate expiration for time-specific orders if type_time in (self.mt5_instance.ORDER_TIME_SPECIFIED, self.mt5_instance.ORDER_TIME_SPECIFIED_DAY) and expiration is None: print(f"Expiration required for order type {type_time}") return False request = { "action": self.mt5_instance.TRADE_ACTION_PENDING, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "sl": sl, "tp": tp, "deviation": self.deviation_points, "magic": self.magic_number, "comment": comment[:31], # MT5 comment max length is 31 chars "type_time": type_time, "type_filling": self.filling_type, } # Add expiration if required if type_time in (self.mt5_instance.ORDER_TIME_SPECIFIED, self.mt5_instance.ORDER_TIME_SPECIFIED_DAY) and expiration is not None: # Convert to broker's expected format (UTC timestamp in milliseconds) expiration_utc = expiration.astimezone(timezone.utc) if expiration.tzinfo else expiration.replace(tzinfo=timezone.utc) request["expiration"] = int(expiration_utc.timestamp() * 1000) # Send order if self.simulator.order_send(request) is None: return False self.__GetLogger().info(f"Order opened successfully!") return True def buy(self, volume: float, symbol: str, price: float, sl: float=0.0, tp: float=0.0, comment: str="") -> bool: """ Opens a buy (market) position. Args: volume: Trade volume (lot size) symbol: Trading symbol (e.g., "EURUSD") price: Execution price sl: Stop loss price (optional, default=0.0) tp: Take profit price (optional, default=0.0) comment: Position comment (optional, default="") Returns: bool: True if order was sent successfully, False otherwise """ return self.position_open(symbol=symbol, volume=volume, order_type=self.mt5_instance.ORDER_TYPE_BUY, price=price, sl=sl, tp=tp, comment=comment)
Mit diesem Modul, das an die Bedürfnisse unseres Simulators angepasst wurde, können wir nun auf einfache Weise Positionen und schwebende Aufträge eröffnen. Bevor wir diese Methoden testen, sollten wir die Änderungen an unserer Klasse verstehen und wissen, was zu tun ist, damit ein Simulator erfolgreich läuft.
Im vorigen Artikel haben wir eine Methode namens Start eingeführt, die unsere Simulatorklasseninstanz in einen Strategietestermodus versetzt, der alles und fast alle Daten in der Klasse virtuell macht.
def Start(self, IS_TESTER: bool) -> bool: # simulator start self.IS_TESTER = IS_TESTER
Wenn dieser Modus ausgewählt ist (ist auf „True“ gesetzt), ahmt der Simulator das Verhalten des MetaTrader 5-Strategietesters nach; bei der Einstellung „False“ verhält sich der Simulator anders, da er für alle Informationen direkt auf den MetaTrader 5-Client zurückgreift und die Trades dort eröffnet. Dieser Modus wurde zu Testzwecken in die Klasse integriert, um sicherzustellen, dass die Abläufe im Simulator denen im MetaTrader 5-Client entsprechen.
Mit den neu eingeführten Änderungen wird diese Methode abgeschafft. Standardmäßig wird die Klasse im Modus Strategietester (Simulator) aufgerufen. Um in den MetaTrader 5-Modus zu gelangen (in der Regel zu Debugging-Zwecken), muss der Nutzer beim Aufruf des endgültigen Skripts das Argument --mt5 übergeben.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
Ausführen von Handelsaktionen in einem Simulator
Sie müssen die folgenden Schritte ausführen, um die aktuelle Version des Simulators zu testen (Dateien am Ende dieses Artikels):
Innerhalb von test.py
01: Initialisieren des MetaTrader 5-Terminals
import MetaTrader5 as mt5 from Trade.Trade import CTrade from datetime import datetime, timedelta import time import pytz from simulator import Simulator, CTrade if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit()
02: Aufrufen der Simulatorinstanz
Wir rufen die Instanz der Simulatorklasse auf und geben ihr die Instanz der MetaTrader 5-App, den Kontostand, der als Einlage bezeichnet wird, und den Hebelwert.
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")
Wir verwenden dann eine Simulatorinstanz für die Klasse CTrade.
03: (Optional) Instanziierung der CTrade-Klasse
symbol = "EURUSD"
timeframe = mt5.TIMEFRAME_H1 m_trade = CTrade(simulator=sim, magic_number=112233, filling_type_symbol=symbol, deviation_points=100)
04: Wir weisen dem Simulator Tick-Informationen zu und geben sie zurück
mt5_ticks = mt5.symbol_info_tick(symbol) # tick source sim.TickUpdate(symbol=symbol, tick=mt5_ticks) # very important tick_from_sim = sim.symbol_info_tick(symbol=symbol) # we get ticks back from a class ask = tick_from_sim.ask bid = tick_from_sim.bid
05: Schließlich, einige Handelsgeschäfte
symbol_info = sim.symbol_info(symbol=symbol) lotsize = symbol_info.volume_min m_trade.buy( volume=lotsize, symbol=symbol, price=ask, sl=ask - 100 * symbol_info.point, tp=ask + 150 * symbol_info.point, comment="Market Buy" ) m_trade.sell( volume=lotsize, symbol=symbol, price=bid, sl=bid + 100 * symbol_info.point, tp=bid - 150 * symbol_info.point, comment="Market Sell" ) buy_limit_price = ask - 200 * symbol_info.point m_trade.buy_limit( volume=lotsize, symbol=symbol, price=buy_limit_price, sl=buy_limit_price - 100 * symbol_info.point, tp=buy_limit_price + 200 * symbol_info.point, comment="Buy Limit" ) sell_limit_price = bid + 200 * symbol_info.point m_trade.sell_limit( volume=lotsize, symbol=symbol, price=sell_limit_price, sl=sell_limit_price + 100 * symbol_info.point, tp=sell_limit_price - 200 * symbol_info.point, comment="Sell Limit" ) buy_stop_price = ask + 150 * symbol_info.point m_trade.buy_stop( volume=lotsize, symbol=symbol, price=buy_stop_price, sl=buy_stop_price - 100 * symbol_info.point, tp=buy_stop_price + 300 * symbol_info.point, comment="Buy Stop" ) sell_stop_price = bid - 150 * symbol_info.point m_trade.sell_stop( volume=lotsize, symbol=symbol, price=sell_stop_price, sl=sell_stop_price + 100 * symbol_info.point, tp=sell_stop_price - 300 * symbol_info.point, comment="Sell Stop" )
Wir können prüfen, ob diese offenen Positionen und Aufträge in unserem Simulator existieren.
print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) print(f"orders in a simulator = {sim.orders_total()}:\n", sim.orders_get())
Ausgänge (Testermodus):
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py
2026-01-05 15:09:24,504 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:24,513 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
positions in a simulator = 2:
(TradePosition(ticket=113127357728313068862, time=1767622158, time_msc=1767622158000, time_update=1767622158, time_update_msc=1767622158000, type=0, magic=112233, identifier=113127357728313068862, reason=3, volume=0.01, price_open=1.16792, sl=1.1669200000000002, tp=1.1694200000000001, price_current=1.16792, swap=0, profit=0, symbol='EURUSD', comment='Market Buy', external_id=''),
TradePosition(ticket=113127357728890995262, time=1767622158, time_msc=1767622158000, time_update=1767622158, time_update_msc=1767622158000, type=1, magic=112233, identifier=113127357728890995262, reason=3, volume=0.01, price_open=1.16792, sl=1.16892, tp=1.16642, price_current=1.16792, swap=0, profit=0, symbol='EURUSD', comment='Market Sell', external_id=''))
orders in a simulator = 4:
(TradeOrder(ticket=113127357729019468800, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16592, sl=1.1649200000000002, tp=1.16792, price_current=1.16592, price_stoplimit=0, symbol='EURUSD', comment='Buy Limit', external_id=''),
TradeOrder(ticket=113127357729019468835, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16992, sl=1.17092, tp=1.16792, price_current=1.16992, price_stoplimit=0, symbol='EURUSD', comment='Sell Limit', external_id=''),
TradeOrder(ticket=113127357729019468836, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=4, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.1694200000000001, sl=1.1684200000000002, tp=1.17242, price_current=1.1694200000000001, price_stoplimit=0, symbol='EURUSD', comment='Buy Stop', external_id=''),
TradeOrder(ticket=113127357729019468803, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=5, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16642, sl=1.16742, tp=1.1634200000000001, price_current=1.16642, price_stoplimit=0, symbol='EURUSD', comment='Sell Stop', external_id='')) Ausgänge (MetaTrader 5 Modus):
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
2026-01-05 15:09:29,171 | INFO | simulator | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:30,270 | INFO | simulator | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:31,110 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:31,711 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:33,000 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:33,952 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
positions in a simulator = 2:
(TradePosition(ticket=1393244663, time=1767622166, time_msc=1767622166713, time_update=1767622166, time_update_msc=1767622166713, type=0, magic=112233, identifier=1393244663, reason=3, volume=0.01, price_open=1.16791, sl=1.1669100000000001, tp=1.16941, price_current=1.16791, swap=0.0, profit=0.0, symbol='EURUSD', comment='Market Buy', external_id=''),
TradePosition(ticket=1393244666, time=1767622167, time_msc=1767622167817, time_update=1767622167, time_update_msc=1767622167817, type=1, magic=112233, identifier=1393244666, reason=3, volume=0.01, price_open=1.16791, sl=1.16891, tp=1.16641, price_current=1.16791, swap=0.0, profit=0.0, symbol='EURUSD', comment='Market Sell', external_id=''))
orders in a simulator = 4:
(TradeOrder(ticket=1393244672, time_setup=1767622168, time_setup_msc=1767622168661, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16591, sl=1.16491, tp=1.16791, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Buy Limit', external_id=''),
TradeOrder(ticket=1393244676, time_setup=1767622169, time_setup_msc=1767622169494, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16991, sl=1.1709100000000001, tp=1.16791, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Sell Limit', external_id=''),
TradeOrder(ticket=1393244679, time_setup=1767622170, time_setup_msc=1767622170093, time_done=0, time_done_msc=0, time_expiration=0, type=4, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16941, sl=1.16841, tp=1.17241, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Buy Stop', external_id=''),
TradeOrder(ticket=1393244687, time_setup=1767622171, time_setup_msc=1767622171748, time_done=0, time_done_msc=0, time_expiration=0, type=5, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16641, sl=1.16741, tp=1.16341, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Sell Stop', external_id=''))
Verwaltung von Aufträgen und Positionen in einem Simulator
Mit Positionen, die in Arrays oder Containern in einer Simulatorklasse gespeichert sind, können wir Aktionen durchführen, z. B. bestimmte Verhaltensweisen/Zustände erkennen, sie ändern oder sogar schließen.
01: Positionen schließen
Wir eröffnen zwei verschiedene Positionen und schließen eine.
m_trade.buy(volume=lotsize, symbol=symbol, price=ask, comment="buy pos") m_trade.sell(volume=lotsize, symbol=symbol, price=bid, comment="sell pos") print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) positions = sim.positions_get() for pos in positions: if pos.symbol == symbol and pos.type == sim.mt5_instance.POSITION_TYPE_BUY: # close a buy position m_trade.position_close(ticket=pos.ticket, deviation=10) print("positions remaining: ", sim.positions_get())
Ausgabe:
2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! 2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! positions in a simulator = 2: (TradePosition(ticket=113127532167305337632, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=0, magic=112233, identifier=113127532167305337632, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127532167305337651, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=1, magic=112233, identifier=113127532167305337651, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id='')) 2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:397 - position_close() ] => Position 113127532167305337632 closed successfully! positions remaining: (TradePosition(ticket=113127532167305337651, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=1, magic=112233, identifier=113127532167305337651, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id=''),)
Es wurde ausschließlich eine Kaufposition geschlossen!
02: Änderungen der Position
Ähnlich wie bei der Schließung von Positionen müssen wir eine auswählen, bevor wir eine Änderungsanfrage senden.
m_trade.buy(volume=lotsize, symbol=symbol, price=ask, comment="buy pos") m_trade.sell(volume=lotsize, symbol=symbol, price=bid, comment="sell pos") print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) positions = sim.positions_get() for pos in positions: if pos.sl == 0: if pos.type == sim.mt5_instance.POSITION_TYPE_BUY: m_trade.position_modify(ticket=pos.ticket, sl=pos.price_open - 100 * symbol_info.point, tp=pos.tp) if pos.type == sim.mt5_instance.POSITION_TYPE_SELL: m_trade.position_modify(ticket=pos.ticket, sl=pos.price_open + 100 * symbol_info.point, tp=pos.tp) print("positions after modification\n: ", sim.positions_get())
Ausgabe:
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! positions in a simulator = 2: (TradePosition(ticket=113127856102580262436, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=0, magic=112233, identifier=113127856102580262436, reason=3, volume=0.01, price_open=1.16789, sl=0.0, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127856102710534416, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=1, magic=112233, identifier=113127856102710534416, reason=3, volume=0.01, price_open=1.16789, sl=0.0, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id='')) 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:469 - position_modify() ] => Position 113127856102580262436 modified successfully! 2026-01-05 17:19:11,606 | INFO | tester | [Trade.py:469 - position_modify() ] => Position 113127856102710534416 modified successfully! positions after modification : (TradePosition(ticket=113127856102580262436, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=0, magic=112233, identifier=113127856102580262436, reason=3, volume=0.01, price_open=1.16789, sl=1.1668900000000002, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127856102710534416, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=1, magic=112233, identifier=113127856102710534416, reason=3, volume=0.01, price_open=1.16789, sl=1.16889, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id=''))
03: Arbeiten mit schwebenden Aufträgen
Nachfolgend erfahren Sie, wie Sie schwebende Aufträge ändern und löschen können.
m_trade.buy_stop(symbol=symbol, volume=symbol_info.volume_min, price=ask+500*symbol_info.point) for order in sim.orders_get(): print("order curr price: ", order.price_open) m_trade.order_modify(ticket=order.ticket, price=order.price_open+10*symbol_info.point, sl=order.sl, tp=order.tp) print("order moved 10 points upward", order.price_open) if m_trade.order_delete(ticket=order.ticket) is None: continue print("orders remaining: ", sim.orders_total())
Ausgabe:
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully! order curr price: 1.17281 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:536 - order_modify() ] => Order 113127948523388723206 modified successfully! order moved 10 points upward 1.17291 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:431 - order_delete() ] => Order 113127948523388723206 deleted successfully! orders remaining: 0
Was kommt als Nächstes?
Die Möglichkeit, Tick-Daten aus einem bestimmten Zeitraum in der Vergangenheit zu verwenden und damit eine Handelsoperation zu simulieren, bedeutet, dass wir nur noch wenige Schritte von der Fertigstellung unseres nutzerdefinierten Simulators entfernt sind. Jetzt haben wir alles, was wir benötigen, um eine Schleife zu erstellen, die die Simulation durch alle verfügbaren Ticks im angegebenen Zeitbereich laufen lässt. Dies nennen wir Strategietests oder eine vollständige Simulation.
In diesem Artikel haben wir den wichtigsten Aspekt eines Handelssimulators besprochen, nämlich das Senden von Handelsaufträgen und deren Verwaltung. In der nächsten Ausgabe werden wir alles zusammenfügen und unsere allererste Strategietestaktion in Python durchführen. Bleiben Sie dran!
Tschüss.
Teilen Sie Ihre Gedanken und helfen Sie, dieses Projekt auf GitHub zu verbessern: https://github.com/MegaJoctan/PyMetaTester
Tabelle der Anhänge
| Dateiname | Beschreibung und Verwendung |
|---|---|
| Trade/Trade.py | Enthält die Klasse CTrade, eine Klasse, die eine einfache Möglichkeit zur Ausführung von Handelsoperationen bietet. |
| config.py | Eine Python-Konfigurationsdatei, in der die nützlichsten Variablen für die Wiederverwendbarkeit im gesamten Projekt definiert sind. |
| utils.py | Eine Python-Datei mit Hilfsprogrammen, die einfache Funktionen zur Bewältigung verschiedener Aufgaben enthält (Helfer). |
| simulator.py | Die Datei beinhaltet eine Klasse namens Simulator. Unsere zentrale Simulatorlogik befindet sich an einem Ort. |
| test.py | Eine Datei zum Testen des gesamten Codes und der Funktionen, die in diesem Beitrag besprochen werden. |
| error_description.py | Es verfügt über Funktionen zur Umwandlung aller MetaTrader 5-Fehlercodes in menschenlesbare Meldungen. |
| requirements.txt | Enthält alle Python-Abhängigkeiten und deren Versionen, die in diesem Projekt verwendet werden. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/20782
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.
Larry Williams‘ Geheimnisse des Marktes (Teil 7): Eine empirische Untersuchung zum Konzept des Handelstages der Woche
Einführung in MQL5 (Teil 33): Beherrschen der API- und WebRequest-Funktion in MQL5 (VII)
Einführung in MQL5 (Teil 34): Beherrschung der API- und WebRequest-Funktion in MQL5 (VIII)
Larry Williams’ Geheimnisse des Marktes (Teil 6): Messung von Volatilitätsausbrüchen anhand der Swings des Marktes
- 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.