Тестер стратегий для Python и MetaTrader 5 (Часть 03): Обработка и управление торговыми операциями по образцу MetaTrader 5
Оглавление
- Введение
- Метод order_send
- Класс проверки торговых операций
- Все валидаторы внутри order_send
- Класс CTrade внутри симулятора
- Выполнение торговых действий в симуляторе
- Управление ордерами и позициями в симуляторе
- Заключение
Введение
В предыдущей статье мы реализовали в нашем симуляторе синтаксис и функции, похожие на те, которые предлагает модуль Python–MetaTrader. Также были созданы аналогичные ордера, сделки, позиции и структуры. В этой статье мы реализуем подход к обработке таких структур, то есть торговых операций, максимально близкий к MetaTrader 5.

Метод order_send
Все торговые действия в MetaTrader 5, такие как размещение отложенных ордеров, открытие позиций на покупку или продажу, изменение и удаление ордеров, выполняются через вызов одной стандартной функции.
В MQL5 эта функция называется OrderSend, а в Python–MetaTrader 5 — order_send.
Согласно документации:
Метод order_send отправляет запрос на выполнение торговой операции из терминала на торговый сервер. Он аналогичен OrderSend.
order_send(
request // request structure
); Он принимает один параметр под названием request. Структура типа MqlTradeRequest описывает требуемое торговое действие.
В следующей таблице представлены поля, которые должна содержать "структура запроса".
| Поле | Описание |
|---|---|
| action | Тип торговой операции. Значение может быть одним из значений перечисления TRADE_REQUEST_ACTIONS. |
| magic | Идентификатор советника. Позволяет организовать аналитическую обработку торговых ордеров. Каждый советник может задать уникальный ID при отправке торгового запроса. |
| order | Тикет ордера. Требуется для изменения отложенных ордеров. |
| symbol | Название торгового инструмента, по которому размещается ордер. Не требуется при изменении ордеров и закрытии позиций. |
| volume | Запрашиваемый объем сделки в лотах. Фактический объем при совершении сделки зависит от типа исполнения ордера. |
| price | Цена, по которой должен быть исполнен ордер. Цена не задается в случае рыночных ордеров для инструментов с типом исполнения Market Execution (SYMBOL_TRADE_EXECUTION_MARKET), имеющих тип TRADE_ACTION_DEAL. |
| stoplimit | Цена, по которой будет выставлен отложенный лимитный ордер после того, как цена достигнет значения price (это условие обязательно). До этого момента ордер не передаётся в торговую систему. До этого момента отложенный ордер не передается в торговую систему. |
| sl | Цена, при которой активируется ордер Stop Loss, если цена движется в неблагоприятном направлении. |
| tp | Цена, при которой активируется ордер Take Profit, если цена движется в благоприятном направлении. |
| deviation | Максимально допустимое отклонение от запрошенной цены, указанное в пунктах. |
| type | Тип ордера. Значение может быть одним из значений перечисления ORDER_TYPE. |
| type_filling | Тип исполнения ордера. Значение может быть одним из значений ORDER_TYPE_FILLING. |
| type_time | Тип ордера по сроку действия. Значение может быть одним из значений ORDER_TYPE_TIME. |
| expiration | Время истечения срока действия отложенного ордера для ордеров типа TIME_SPECIFIED. |
| comment | Комментарий к ордеру. |
| position | Тикет позиции. Заполняется при изменении и закрытии позиции для ее точной идентификации. Обычно совпадает с тикетом ордера, который открыл позицию. |
| position_by | Тикет противоположной позиции. Используется при закрытии позиции путем открытия позиции в противоположном направлении по тому же символу. |
Нам нужна аналогичная функция в нашем классе.
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
В предыдущей статье мы добавили в класс симулятора режим тестера стратегий, то есть режим, когда переменная IS_TESTER = True. Если этот режим не используется, симулятор опирается на информацию из клиента MetaTrader 5, а также открывает и управляет всеми торговыми операциями там.
Приведенный выше фрагмент кода отправляет запрос на выставление ордера в MetaTrader 5, когда пользователь не находится в режиме тестера.
В противном случае мы извлекаем параметры запроса.
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)
Мы должны вручную обрабатывать все операции внутри этой функции, включая запись сделок, а также открытие и закрытие позиций с сохранением данных в контейнерах, то есть, по сути, выполнять работу MetaTrader 5.
I: Размещение отложенных ордеров
В симуляторе отложенный ордер — это просто информация об ордере, сохраненная во временном массиве-контейнере ордеров (self.__orders_container__).
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="", )
После создания ордера мы добавляем его в массив-контейнер ордеров, а также записываем его в массив истории ордеров.
self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, }
II: Открытие позиций
В MetaTrader 5 позиции — это контракты, купленные или проданные по финансовому инструменту. Длинная позиция, или long, формируется в результате покупки в ожидании роста цены. Короткая позиция, или short, является результатом продажи актива в ожидании его будущего снижения.
В симуляторе позиция — это набор информации, похожей на данные позиции, сохраненный в контейнере.
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)
Процессы открытия и закрытия позиций имеют действие TRADE_ACTION_DEAL. Результат такой операции можно назвать сделкой, поэтому нам нужно записать такую запись в контейнер сделок.
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, }
Сделка — это просто запись в истории об открытии и закрытии позиций в терминале MetaTrader 5.

III: Закрытие позиций
Запрос на закрытие позиции очень похож на запрос на открытие новой позиции. Оба являются сделками, но с разными значениями entry.
Чтобы определить запрос на закрытие позиции, мы проверяем, есть ли в запросе ключ 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, }
Однако мы не можем слепо принимать любой запрос на закрытие позиции. Такие запросы необходимо проверять.
На данный момент есть две важные детали, которые нужно проверить перед записью сделки и удалением позиции из контейнера:
- Проверка того, что в запросе указана корректная цена. В MetaTrader 5 позиции Buy закрываются по цене Bid, а позиции Sell — по цене Ask.
- Проверка того, что тип ордера в запросе является противоположным типу существующей позиции. То есть если запрос отправлен для существующей позиции Buy (ORDER_TYPE_BUY), она должна закрываться с помощью ORDER_TYPE_SELL.
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: Изменение позиций
Чтобы изменить позицию, мы фокусируемся только на двух параметрах: Stop Loss и 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: Удаление отложенных ордеров
Это простой процесс удаления ордера из массива-контейнера. Проверки здесь не требуются.
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: Изменение отложенных ордеров
Чтобы изменить отложенный ордер, мы фокусируемся на пяти важных параметрах: цене открытия ордера, Stop Loss, Take Profit, времени истечения срока действия и Stop Limit.
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}
Но все эти действия будут некорректными без базового метода или набора методов, которые в первую очередь проверяют параметры запроса.
В первом симуляторе, реализованном нами в этой серии статей, проверка параметров ордеров была организована не лучшим образом. Теперь мы улучшим этот подход, реализовав отдельный класс для этой задачи.
Класс проверки торговых операций
Как известно, MetaTrader 5 принимает не все переданные запросы. Он проверяет их на некорректные параметры и при обнаружении ошибки отклоняет такие ордера.
Эти параметры проверяются с учетом спецификации конкретного инструмента, типа счета, требований брокера, а иногда и ограничений клиента MetaTrader 5.
I: Проверка правильного размера лота
Чтобы размер лота был принят MetaTrader 5:
- он должен быть больше минимально допустимого размера лота для конкретного инструмента;
- он должен быть меньше максимально допустимого размера лота для конкретного инструмента;
- он должен быть кратен шагу объема.
Внутри 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: Проверка наличия достаточных средств для новой позиции
Терминал MetaTrader 5 проверяет, достаточно ли свободной маржи на счете для открытия новой позиции.
Ниже приведена похожая функция для этой задачи.
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: Проверка корректной цены входа
Для позиции Buy цена должна быть равна цене Ask, а для позиции Sell — цене Bid. Эта проверка применяется только к позициям.
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: Проверка того, что Stop Loss и Take Profit не находятся слишком близко к рынку
У каждого символа есть небольшое пороговое значение, показывающее минимальное расстояние, на котором Stop Loss и Take Profit должны находиться от рынка.
Это пороговое значение называется SYMBOL_TRADE_STOPS_LEVEL.
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: Проверка корректности значений Stop Loss и Take Profit
Для ордера Buy значение Stop Loss должно быть ниже цены входа, а Take Profit — выше цены входа.
Для ордера Sell значение Take Profit должно быть ниже цены входа, а Stop Loss — выше цены входа.
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: Проверка того, что максимальный объем по инструменту не достигнут
У некоторых символов у некоторых брокеров есть ограничение на общий размер лота по отдельным открытым ордерам и позициям.
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: Проверка того, что максимальное количество ордеров не достигнуто
Некоторые счета имеют ограничение на количество отложенных ордеров, которые могут быть открыты одновременно. Мы должны проверять это, чтобы не нарушать ограничения симулируемого счета — так же, как MetaTrader 5 не позволил бы нарушить ограничения реального счета.
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: Проверка уровня заморозки (freeze level)
| Тип ордера / позиции | Цена активации | Проверка |
|---|---|---|
| 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 | Bid | TakeProfit-Bid >= SYMBOL_TRADE_FREEZE_LEVEL Bid-StopLoss >= SYMBOL_TRADE_FREEZE_LEVEL |
| позиция Sell | 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
Все валидаторы внутри order_send — кратко
После того как все эти функции были помещены в класс TradeValidators в файле validators.py и применены к функции order_send, все вместе выглядит следующим образом:
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", }
Класс CTrade внутри симулятора
Как и MQL5, модуль Python-MetaTrader 5 ощущается как низкоуровневый модуль, который позволяет нам взаимодействовать с терминалом MetaTrader 5. Как мы только что увидели, для отправки запроса на открытие позиций и ордеров требуется больше действий, чем хотелось бы (долгий и утомительный процесс).
В MQL5 у нас есть так называемые торговые классы, которые предоставляют простой интерфейс для открытия сделок и управления ими. В Python мы создали похожие классы; теперь адаптируем класс CTrade под нужды нашего симулятора.
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
Мы передаём нашему классу экземпляр симулятора, чтобы получать некоторые методы из него, а не напрямую из MetaTrader 5. Это помогает классу использовать переопределённые методы, которые обсуждались в предыдущей статье.
Класс CTrade содержит множество методов, и все они опираются на метод order_send, который входит в модуль Python-MetaTrader 5.
Во всём классе вместо вызова методов из модуля Python-MetaTrader 5 мы вызываем переопределённые методы из класса Simulator.
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)
Благодаря адаптации этого модуля под нужды симулятора теперь у нас есть простой способ открывать позиции и отложенные ордера. Прежде чем тестировать эти методы, давайте разберёмся, какие изменения были внесены в класс и что необходимо сделать для успешного запуска симулятора.
В предыдущей статье мы представили метод Start, который переводил экземпляр класса симулятора в режим тестера стратегий. В этом режиме всё — или почти все данные внутри класса — становилось виртуальным.
def Start(self, IS_TESTER: bool) -> bool: # simulator start self.IS_TESTER = IS_TESTER
Когда этот режим выбран, то есть установлен в True, симулятор имитирует поведение тестера стратегий MetaTrader 5. Если установить значение False, симулятор уже перестаёт быть полноценным симулятором, поскольку он напрямую полагается на клиент MetaTrader 5 для получения всей информации и открывает сделки именно там. Этот режим был добавлен в класс для целей тестирования, чтобы убедиться, что действия, выполняемые в симуляторе, соответствуют тому, что происходит в клиенте MetaTrader 5.
После внедрения новых изменений этот метод был удалён. По умолчанию класс вызывается в режиме тестера стратегий, то есть в режиме симулятора. Чтобы перейти в режим MetaTrader 5, обычно для отладки, пользователь должен передать аргумент --mt5 при запуске итогового скрипта.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
Выполнение торговых действий в симуляторе
Чтобы протестировать текущую версию симулятора, необходимо выполнить следующие шаги. Файлы прикреплены в конце статьи.
Внутри test.py
01: Инициализация терминала MetaTrader 5
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: Создание экземпляра симулятора
Мы создаём экземпляр класса симулятора, передавая ему экземпляр приложения MetaTrader 5, баланс счёта, обозначенный как deposit, и значение кредитного плеча.
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")
Затем мы используем экземпляр симулятора для класса CTrade.
03: Необязательно: создание экземпляра класса CTrade
symbol = "EURUSD"
timeframe = mt5.TIMEFRAME_H1 m_trade = CTrade(simulator=sim, magic_number=112233, filling_type_symbol=symbol, deviation_points=100)
04: Передача тиковой информации в симулятор и получение её обратно
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: Наконец, выполнение торговых операций
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" )
Мы можем проверить, существуют ли эти открытые позиции и ордера в нашем симуляторе.
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())
Вывод, режим тестера:
(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='')) Вывод, режим MetaTrader 5
(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=''))
Управление ордерами и позициями в симуляторе
С позициями, хранящимися внутри массивов — контейнеров в классе симулятора, — мы можем выполнять над ними действия: например, обнаруживать определённое поведение или условия, изменять их или даже закрывать.
01: Закрытие позиций
Давайте откроем две отдельные позиции и закроем одну из них.
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())
Вывод:
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=''),)
Была закрыта только позиция Buy.
02: Изменение позиций
Как и при закрытии позиций, нам нужно выбрать одну позицию, прежде чем отправлять ей запрос на изменение.
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())
Выводы:
(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: Работа с отложенными ордерами
Ниже показано, как можно изменять и удалять отложенные ордера.
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())
Выводы:
(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
Что дальше?
Возможность использовать тиковые данные за определённый период в прошлом и с их помощью имитировать торговую операцию означает, что мы уже в нескольких шагах от завершения нашего собственного симулятора. Сейчас у нас есть всё необходимое, чтобы создать цикл, который прогонит симуляцию по всем доступным тикам в заданном временном диапазоне. Именно это мы называем тестированием стратегии или полной симуляцией.
В этой статье мы обсудили самый важный аспект любого торгового симулятора — отправку торговых запросов и управление ими. В следующей статье мы соберём всё вместе и запустим наше самое первое тестирование стратегии на Python. Следите за обновлениями!
До встречи в следующей статье.
Поделитесь своими мыслями и помогите улучшить этот проект на GitHub: https://github.com/MegaJoctan/PyMetaTester
Таблица вложений
| Название файла | Описание и использование |
|---|---|
| Trade/Trade.py | Содержит класс CTrade — класс, предоставляющий удобный способ выполнения торговых операций. |
| config.py | Файл конфигурации Python, в котором определены наиболее полезные переменные для повторного использования в проекте. |
| utils.py | Вспомогательный Python-файл, содержащий простые функции для помощи в выполнении различных задач. |
| simulator.py | Содержит класс Simulator. В нём собрана основная логика нашего симулятора. |
| test.py | Файл, используемый для тестирования всего кода и функций, рассмотренных в этой публикации. |
| error_description.py | Содержит функции для преобразования всех кодов ошибок MetaTrader 5 в понятные человеку сообщения. |
| requirements.txt | Содержит все Python-зависимости и их версии, используемые в этом проекте. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20782
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Тестер стратегий для Python и MetaTrader 5 (Часть 04): Основы работы тестера
Как заменить WebSocket EA на TradeMux REST в MetaTrader 5
Переосмысливаем классические стратегии (Часть 15): Стратегия пробоя диапазона предыдущего дня
Автоматизация торговых стратегий в MQL5 (Часть 27): Выявление и визуализация гармонического паттерна "Краб" на основе Price Action
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования