English
preview
Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор

Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор

MetaTrader 5Торговые системы |
318 1
Omega J Msigwa
Omega J Msigwa

Содержание


Лучше делать что-то, чем ничего не делать, ожидая возможности сделать всё.

— Уинстон Черчилль.


Введение

Пакет MetaTrader5-Python — полезный модуль, который позволяет разработчикам Python создавать торговые приложения для платформы MetaTrader 5. Он предоставляет разработчикам доступ к торговой платформе для получения данных, отправки и мониторинга сделок.

Этот модуль изменил наше представление о настольном приложении MetaTrader 5: это не узкоспециализированное приложение, ограниченное родным языком программирования для создания торговых роботов, известных как MQL5. Это приложение достаточно гибкое и способно принимать торговые команды из внешнего языка программирования, отличного от MQL5.

Хотя модуль MetaTrader5 дает нам возможность открывать сделки на платформе MetaTrader 5 с помощью Python, ему не хватает одной важнейшей возможности, которая есть у всех торговых приложений на базе MQL5 — возможности тестировать полностью разработанное торговое приложение в Тестере стратегий.

Можете ли вы представить, что создали торгового робота, но не можете его протестировать? 

Хотя в Python нет недостатка в полезных модулях — существует множество модулей, библиотек и фреймворков для тестирования так называемых торговых стратегий  таких как Backtrader, а также Backtesting.py. Проблема этих Python-инструментов в том, что они были созданы для тестирования простых или иногда индикаторных торговых стратегий.

Они оценивают результаты торговли исключительно на основе сигналов. Они не учитывают такие факторы, как брокерские условия, комиссии, ограничения счета, параметры инструмента, кредитное плечо и другие важные детали, которые учитывает тестер стратегий MetaTrader 5.

Модуль MetaTrader5-Python предназначен для того, чтобы предоставить пользователям базовую возможность получать из платформы ключевую информацию и быстро начать работу с ней на Python.

Опираясь на имеющиеся данные и понимание того, как работает тестер стратегий MetaTrader 5, в этой короткой серии статей мы создадим и реализуем удобный (похожий на тестер MetaTrader 5) способ тестирования наших торговых роботов на Python.

Начните с установки всех зависимостей Python, указанных в файле с именем requirements.txt прикрепленном в конце этой статьи.

pip install -r requirements.txt 


Торговый симулятор 101

Чтобы получить возможность тестировать торговые стратегии в Python, нам нужно создать торговый симулятор. Это похоже на то, что делает Тестер стратегий MetaTrader 5: он моделирует рынок и в процессе запускает приложение или функции (торгового робота или индикатор). 

Не путайте: сам Тестер стратегий, предлагаемый приложением MetaTrader 5, является торговым симулятором.

Мы не будем реализовывать графический интерфейс пользователя (GUI), как в Тестере стратегий (по крайней мере пока). Давайте реализуем для этой задачи класс Python.

import MetaTrader5 as mt5

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

Цель — получить конструктор класса, похожий на конфигурацию Тестера стратегий MetaTrader 5.

  • Переменная mt5_instance очень важна, поскольку помогает отслеживать выбранный экземпляр приложения MetaTrader 5. 
  • Переменная simulator_name  может использоваться для создания папок и путей, которые помогут различать торговые симуляторы; думайте об этой переменной как об имени торгового робота (советника или индикатора).

В классе торгового симулятора нам нужен способ отслеживать информацию обо всех открытых ордерах, позициях и исполненных сделках аналогично тому, как это делает MetaTrader 5.

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

        # .... other variables
        # ...
        # ...
        
        # Position's information
        
        self.position_info = {
            "time": None,
            "id" : 0,
            "magic": 0,
            "symbol": None,
            "type": None,
            "volume": 0.0,
            "open_price": 0.0,
            "price": 0.0,
            "sl": 0.0,
            "tp": 0.0,
            "commission": 0.0,
            "margin_required": 0.0,
            "fee": 0.0,
            "swap": 0.0,
            "profit": 0,
            "comment": 0
        }
        
        # Order's information
        
        self.order_info = self.position_info.copy()
        self.order_info["expiry_date"] = datetime
        self.order_info["expiration_mode"] = ""
        
        # Deal's information

        self.deal_info = self.position_info.copy()
        
        self.deal_info["reason"] = None # This is used to store the reason why the trade was closed, e.g. "Take Profit", "Stop Loss", etc.
        self.deal_info["direction"] = None # The only difference btn an open trade and a closed one is that the closed one has a direction showing if at that instance it was opened or closed in history
        
        # Containers for positions, orders, and deals
                
        self.positions_container = [] # a list for storing all opened trades
        self.deals_container = [] # a list for storing all deals 
        self.orders_container = []

В таблице ниже приведено описание информации о позициях, ордерах и сделках, хранящейся в классе симулятора.

СвойствоОписание
timeВремя исполнения позиции или ордера. Для сделки это время исполнения сделки (входа или выхода).
idУникально увеличиваемый идентификатор всех ордеров, позиций или сделок.
magicМагический номер magic number позиции, ордера или сделки.
symbolИнструмент, по которому была открыта сделка, например (EURUSD, USDJPY).
typeТип позиции для позиций, тип ордера для ордеров. 
volumeТорговый объем (lotsize) примененный к позиции, ордеру или сделке.
open_priceЦена открытия ордера или позиции. Это может быть либо цена закрытия, либо цена открытия сделки, в зависимости от причины сделки.
price Текущая цена на рынке; она равна цене ask для позиции buy или связанных с buy отложенных ордеров,  и цене bid для позиции sell или связанных с sell отложенных ордеров.
slЗначение stop loss ордера, позиции или сделки.
tpЗначение take profit ордера, позиции или сделки.
comission Получает сумму комиссии, взимаемой с позиции.
margin_required Хранит требуемую маржу для исполнения такой позиции или ордера.
fee Содержит брокерские сборы, применяемые к позиции.
swapХранит сумму свопа, примененного к позиции.
profit Хранит рассчитанную прибыль/убыток позиции или сделки.
comment Хранит комментарий позиции, ордера или сделки. 
expiration_mode Хранит SYMBOL_EXPIRATION_MODE для отложенных ордеров (self.order_info).  
expiry_dateХранит время истечения ордера в формате UTC. 

Вся информация об открытых позициях, размещенных отложенных ордерах и исполненных сделках затем сохраняется в соответствующих массивах симулятора для более удобного доступа.

        # Containers for positions, orders, and deals
                
        self.positions_container = [] # a list for storing all opened trades
        self.deals_container = [] # a list for storing all deals 
        self.orders_container = [] # for storing all pending orders placed


Расчет прибыли/убытка, полученного по позиции

Главная цель моделирования всей торговой активности с точки зрения трейдера — определить прибыль/убытки, которые мог бы получить торговый робот с определенного момента в истории.

Ниже приведена универсальная функция для этой задачи.

def _calculate_profit(self, action: str, symbol: str, entry_price: float, exit_price: float, lotsize: float) -> float:
    
    """
    Calculate profit based on entry and exit prices, lot size, tick size, and tick value.
    
    Args:
        action (str): The action taken, either 'buy' or 'sell'.
        entry_price (float): The price at which the position was opened.
        exit_price (float): The price at which the position was closed.
        lotsize (float): The size of the lot in terms of contract units.
    """

    if action != "buy" and action != "sell":
        print(f"Unknown order type, It can be either 'buy' or 'sell'. Received '{action}' instead.")
        return 0
    
    order_type = self.mt5_instance.ORDER_TYPE_BUY if action == "buy" else self.mt5_instance.ORDER_TYPE_SELL
    
    profit = self.mt5_instance.order_calc_profit(
        order_type,
        symbol,
        lotsize,
        entry_price,
        exit_price
    )
    
    return profit

Мы будем использовать эту функцию для расчета прибыли/убытков всех рыночных ордеров (позиций) в MetaTrader 5, передавая ей цену входа и выхода, инструмент (symbol) и размер лота.

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

sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1000, leverage="1:500")
profit = sim._calculate_profit(action="buy", 
                            symbol="EURUSD", 
                            entry_price=1.17246, 
                            exit_price=1.17390,
                            lotsize=0.07)

print("profit: ", profit)

Вывод.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy Tester/trade_simulator.py"
profit:  10.08


Симуляция позиции

В торговом моделировании позиция — это просто набор рассчитанной информации, похожей на сделку, которая хранится в памяти или на диске. 

Ниже приведена базовая функция для открытия позиций.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        self.m_symbol.name(symbol)

        self.id += 1  # Increment trade ID

        trade_info["time"] = self.time
        trade_info["id"] = self.id
        trade_info["magic"] = self.magic_number
        trade_info["symbol"] = symbol
        trade_info["type"] = pos_type
        trade_info["volume"] = volume
        trade_info["price"] = price
        trade_info["sl"] = sl
        trade_info["tp"] = tp
        trade_info["commission"] = 0.0
        trade_info["fee"] = 0.0
        trade_info["swap"] = 0.0
        trade_info["profit"] = 0.0
        trade_info["comment"] = comment
        trade_info["margin_required"] = self._calculate_margin(symbol=symbol, volume=volume, price=price)

        # Append to open trades
        self.open_trades_container.append(trade_info)
        print("Trade opened successfully: ", trade_info)

        return True

Снова, свойство id, которое соответствует тикету позиции, автоматически увеличивается, чтобы создать уникальный номер тикета для каждой позиции, открытой в экземпляре класса.

Свойство с именем margin_required оказалось самым сложным для реализации, потому что, хотя модуль MetaTrader5 предоставляет функцию для помощи в расчете маржи, она учитывает текущий авторизованный счет в приложении MetaTrader 5; она использует информацию этого счета, включая кредитное плечо.

Поскольку нам нужен симулируемый счет в этом Python-симуляторе, нужна пользовательская функция для расчета требуемого значения маржи для каждой позиции согласно заданным параметрам так называемого имитируемого счета.

    def _calculate_margin(self, symbol: str, volume: float, open_price: float, margin_rate=1.0) -> float:
        
        """
        Calculates margin requirement similar to MetaTrader5 based on the margin mode.
        """
        self.m_symbol.name(symbol)

        if not self.m_symbol.select():
            print(f"Margin calculation failed: MetaTrader5 error = {self.mt5_instance.last_error()}")
            return 0.0

        contract_size = self.m_symbol.contract_size()
        leverage = self.leverage
        margin_mode = self.m_symbol.trade_calc_mode()

        print("Margin calculation mode: ",self.m_symbol.trade_calc_mode_description())
        
        tick_size = self.m_symbol.tick_size() or 0.0001
        tick_value = self.m_symbol.tick_value() or 0.0
        initial_margin = self.m_symbol.margin_initial() or 0.0
        face_value = self.m_symbol.trade_face_value() 
        
            
        if margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX:
            margin = (volume * contract_size * margin_rate) / leverage

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE:
            margin = volume * contract_size * margin_rate

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFD:
            margin = volume * contract_size * open_price * margin_rate

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDLEVERAGE:
            margin = (volume * contract_size * open_price * margin_rate) / leverage

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_CFDINDEX:
            margin = volume * contract_size * open_price * tick_value / tick_size * margin_rate

        elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX]:
            margin = volume * contract_size * open_price * margin_rate

        elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_FUTURES, 
                             self.mt5_instance.SYMBOL_CALC_MODE_EXCH_FUTURES]:
            
            margin = volume * initial_margin * margin_rate

        elif margin_mode in [self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS, self.mt5_instance.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX]:
            margin = volume * contract_size * face_value * open_price / 100

        elif margin_mode == self.mt5_instance.SYMBOL_CALC_MODE_SERV_COLLATERAL:
            margin = 0.0

        else:
            print(f"Unknown margin mode: {margin_mode}, falling back to default margin calc.")
            margin = (volume * contract_size * open_price) / leverage

        return margin

Хотя функция не идеальна, учитывая, что я не смог найти способ получить переменную margin_rate  из приложения MetaTrader 5 с помощью модуля MetaTrader5-Python, который, похоже, участвует в формулах расчета маржи используемых в MQL5.

Поскольку переменная недоступна в symbol_info, аргумент с именем margin_rate (по умолчанию 1.0) дает нам способ вручную вставить это значение.

Процесс сохранения позиции в контейнере предполагает, что с заданными параметрами позиции всё в порядке. Это неверно, потому что, как мы знаем, приложение MetaTrader 5 проверяет, соответствует ли сделка определенным требованиям счета, символа и брокера, прежде чем принять ее.

Например, приложение проверяет, не слишком ли близко значения stop loss и take profit находятся к рынку, и отклоняет все сделки, подпадающие под это условие; оно также проверяет, задан ли допустимый размер позиции (объем/лот) и т. д.

Итак, нам нужна функция, которая возвращает логическое значение для проверки всех позиций. Будут приняты только позиции со всеми допустимыми параметрами; остальные будут отклонены.


Проверка торговых параметров

(a) Проверка размера лота

Чтобы проверить размер лота (объем) сделки, мы проверяем три условия.

  1. Если заданный размер лота меньше минимально допустимого объема для символа
  2. Если заданный размер лота больше максимально допустимого объема для данного символа
  3. Если заданный размер лота кратен своему шагу (минимальному шагу изменения объема для исполнения сделки)

    def _position_validation(self,
                       volume: float,
                       symbol: str,
                       pos_type: str,
                       open_price: float, 
                       sl: float = 0.0, 
                       tp: float = 0.0, 
                       expiry_date: datetime = None) -> bool:
        """
        Validates trade parameters similar to MQL5's OrderCheck()
        
        Returns:
            bool: True if validation passes, False with error message if fails
        """
        
        self.m_symbol.name(symbol) # Assign the current symbol to the CSymbolInfo class for accessing its properties    
            
        # Get symbol properties
        symbol_info = self.m_symbol.get_info() # Get the information about the current symbol
        if symbol_info is None:
            print(f"Trade validation failed. MetaTrader5 error = {self.mt5_instance.last_error()}")
            return False
            
        # Validate volume
        
        if volume < self.m_symbol.lots_min(): # check if the received lotsize is smaller than minimum accepted lot of a symbol
            print(f"Trade validation failed: Volume ({volume}) is less than minimum allowed ({self.m_symbol.lots_min()})")
            return False

        if volume > self.m_symbol.lots_max(): # check if the received lotsize is greater than the maximum accepted lot
            print(f"Trade validation failed: Volume ({volume}) is greater than maximum allowed ({self.m_symbol.lots_max()})")
            return False
        
        step_count = volume / self.m_symbol.lots_step() 
        
        if abs(step_count - round(step_count)) > 1e-7: # check if the stoploss is a multiple of the step size
            print(f"Trade validation failed: Volume ({volume}) must be a multiple of step size ({self.m_symbol.lots_step()})")
            return False
            

(b): Проверка цены открытия сделки и проскальзывания

Как и в тестере стратегий MetaTrader 5, мы должны убедиться, что у позиции есть допустимая цена открытия перед ее принятием, т. е. ее цена открытия должна быть очень близка или равна цене ask символа для позиции buy, а для позиции sell — близка или равна цене bid.

Значение проскальзывания (если задано) используется только для сравнения цен, чтобы убедиться, что заданная цена входа близка к цене bid.

        # Validate the opening price
        
        self.m_symbol.refresh_rates() # Get recent ticks information
        
        ask = self.m_symbol.ask()
        bid = self.m_symbol.bid()
        
        if ask is None or bid is None or ask==0 or bid==0:
            print("Trade Validate: Failed to Get Ask and Bid prices, Call the function market_update() to update the simulator with newly simulated price values")
            return False
        
        # Slippage check
        
        actual_price = ask if pos_type == "buy" else bid
        point = self.m_symbol.point()

        # Allowable slippage range (in absolute price)
        
        max_deviation = self.deviation_points * point
        lower_bound = actual_price - max_deviation
        upper_bound = actual_price + max_deviation

        # Check if requested price is within allowed slippage
        
        if not (lower_bound <= open_price <= upper_bound):
            print(f"Trade validation failed: {pos_type.capitalize()} price ({open_price}) is out of slippage range: {lower_bound:.5f} - {upper_bound:.5f}")
            return False

(c): Проверка stop loss и take profit

Не все значения stop loss и take profit рыночных ордеров (позиций) принимаются брокерами MetaTrader 5; некоторые значения могут быть недопустимыми или слишком близкими к рынку для открытия позиции.

Мы используем одну и ту же логику для проверки как stops level так и freeze level.

Сначала мы проверяем, был ли вообще получен подходящий stop loss.

Затем мы убеждаемся, что значение stop loss ниже цены открытия позиции, а take profit выше нее для сделки buy. Для сделки sell делаем наоборот (stop loss должен быть выше цены открытия, а take profit — ниже).

# Validate stop loss and take profit levels
        
if sl > 0:
    if pos_type == "buy" and sl >= open_price:
        print(f"Trade validation failed: Buy stop loss ({sl}) must be below order opening price ({open_price})")
        return False
    if pos_type == "sell" and sl <= open_price:
        print(f"Trade validation failed: Sell stop loss ({sl}) must be above order opening price ({open_price})")
        return False
    if not self._check_stop_level(symbol, open_price, sl, pos_type):
        return False
                
if tp > 0:
    if pos_type == "buy" and tp <= open_price:
        print(f"Trade validation failed: Buy take profit ({tp}) must be above order opening price ({open_price})")
        return False
    if pos_type == "sell" and tp >= open_price:
        print(f"Trade validation failed: Sell take profit ({tp}) must be below order opening price ({open_price})")
        return False
    if not self._check_stop_level(symbol, open_price, tp, pos_type):
        return False

Приведенные выше строки кода можно найти в функции с именем _check_stops_level.

    def _check_stop_level(self, symbol: str, price: float, stop_price: float, pos_type: str) -> bool:
        
        """Check if stop levels comply with broker requirements"""
        
        self.m_symbol.name(symbol)
        
        # Validate symbol
        if not self.m_symbol.select():
            print(f"Failed to check stop level: Symbol {symbol}. MetaTrader5 error = {self.mt5_instance.last_error()}")
            return False
        
        # Check for stops level 
        stop_level = self.m_symbol.stops_level()
        
        if pos_type == "buy":
            if stop_price > price - stop_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {stop_level} points away")
                return False
        else:  # sell
            if stop_price < price + stop_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {stop_level} points away")
                return False
            
        
        # Check for freeze level
        
        freeze_level = self.m_symbol.freeze_level()
        
        if pos_type == "buy":
            if stop_price > price - freeze_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {freeze_level} points away")
                return False
        else:  # sell
            if stop_price < price + freeze_level * self.m_symbol.point():
                print(f"Trade validation failed: Stop level too close. Must be at least {freeze_level} points away")
                return False
            
        return True

Функция выше возвращает False, если было обнаружено недопустимое значение stop loss или take profit для позиции buy или sell. В противном случае она возвращает True.

Наконец, мы вызываем функцию с именем _position_validation внутри базовой функции открытия позиций. Она проверяет корректность позиции перед сохранением в массиве, содержащем все позиции.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        self.m_symbol.name(symbol)

        if not self._position_validation(volume=volume, symbol=symbol, pos_type=pos_type, price=price, sl=sl, tp=tp):
            return False

        self.id += 1  # Increment trade ID

        trade_info["time"] = self.time
        trade_info["id"] = self.id
        
        # ... proceeds to store a trade 

        # Append to open trades
        self.open_trades_container.append(trade_info)
        print("Trade opened successfully: ", trade_info)
        
        return True

Чтобы открывать позиции buy и sell гораздо удобнее, я создал две отдельные функции с именами buy и sell, для открытия позиций buy и sell соответственно. Эти две функции опираются на базовую функцию с именем _open_position единственное отличие между этими двумя функциями и их предшественником — переменная с именем pos_type (для установки типа позиции). Это значение явно задается в следующих функциях ниже.

    def buy(self, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:
        return self._open_position("buy", volume, symbol, price, sl, tp, comment)

    def sell(self, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:
        return self._open_position("sell", volume, symbol, price, sl, tp, comment)

Приведенные выше функции были вдохновлены похожими функциями, доступными в классе CTrade  предоставляемого стандартными торговыми библиотеками языка MQL5.


Изменение позиций

Возможность изменять позиции важна по разным причинам, связанным с торговлей и управлением капиталом. Например, трейдеры часто изменяют значения stop loss в позициях, перемещая их к точке входа или к значению take profit, чтобы снизить убытки или зафиксировать прибыль; это называется trailing stop или breakeven.

Ниже приведена функция, помогающая разработчикам Python изменять позиции в симуляторе.

    def position_modify(self, pos: dict, new_sl: float, new_tp) -> bool:
        
        new_position = pos.copy()
        
        if pos["type"] == "buy":
            if new_sl >= pos["price"]: 
                print("Failed to modify sl, new_sl >= current price")
                return False
        
        if pos["type"] == "sell":
            if new_sl <= pos["price"]: 
                print("Failed to modify sl, new_sl <= current price")
                return False
        
        if not self._check_stops_level(symbol=pos["symbol"], open_price=pos["open_price"], stop_price=new_sl, pos_type=pos["type"]):
            print("Failed to Modify the Stoploss")
            
        if not self._check_stops_level(symbol=pos["symbol"], open_price=pos["open_price"], stop_price=new_tp, pos_type=pos["type"]):
            print("Failed to Modify the Takeprofit")
        
        # new sl and tp values 

        new_position["sl"] = new_sl
        new_position["tp"] = new_tp
        
        # Update the position in a container
        
        for i, p in enumerate(self.positions_container):
            if p["id"] == pos["id"]:
                self.positions_container[i] = new_position
                print(f"Position with id=[{pos['id']}] modified! new_sl={new_sl} new_tp={new_tp}")
                return True

        print("Failed to modify position: ID not found")

        return True

Процесс изменения позиции в MetaTrader 5 имеет некоторые сходства с открытием новой; функция выше гарантирует выполнение двух проверок перед подтверждением изменения позиции.

  1. Проверка того, что новый stop loss допустим согласно типу позиции: новое значение stop loss должно быть больше текущей рыночной цены позиции для позиции buy и наоборот для позиции sell. 
  2. Проверка того, что новые значения stop loss или take profit не слишком близки к рынку.

Пример использования:

Давайте откроем простую позицию buy и изменим ее stop loss. Каждую секунду мы увеличиваем stop loss такой позиции, вычитая 0.005.

stoploss = 500

ask = m_symbol.ask()
point = m_symbol.point()

sim.buy(volume=0.1, symbol=symbol, open_price=ask, sl=ask-stoploss*point)


while True: # constantly monitor trades and account metrics
    
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    
    for pos in sim.get_positions(): # go through all positions, same as in MQL5
        if pos["type"] == "buy" and pos["symbol"] == symbol: # select a buy position for the current symbol
            sim.position_modify(pos=pos, new_sl=pos["sl"]-0.005, new_tp=pos["tp"])   
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    time.sleep(5) # sleep for one second

Вывод.

Position with id=[1] modified! new_sl=1.1320700000000001 new_tp=0.0
Position with id=[1] modified! new_sl=1.1270700000000002 new_tp=0.0
Position with id=[1] modified! new_sl=1.1220700000000003 new_tp=0.0
Position with id=[1] modified! new_sl=1.1170700000000005 new_tp=0.0


Мониторинг позиций

Поскольку позиция — это всего лишь набор информации, временно хранящийся в памяти, эту информацию необходимо постоянно обновлять.

Например, после открытия позиции мы должны обновлять ее текущую прибыль или убыток согласно движениям цены на рынке (последним ценам ask и bid). Кроме того, нужно отслеживать выход каждой позиции: если текущая рыночная цена (bid для позиции buy или ask для позиции sell) достигает уровня stop loss или take profit позиции, позиция закрывается в этой сделке.

(a): Мониторинг прибыли сделки

Используя ранее рассмотренную функцию расчета прибыли по позиции, мы постоянно отслеживаем и обновляем прибыль, полученную каждой позицией.

    def monitor_positions(self, verbose: bool):
        
        # monitoring all open trades
        
        for pos in self.positions_container:
                
            self.m_symbol.name(pos["symbol"])
            self.m_symbol.refresh_rates()
            
            # Get ticks information for every symbol
            
            ask = self.m_symbol.ask()
            bid = self.m_symbol.bid()
            
            # update price information on all positions
            
            pos["price"] = ask if pos["type"] == "buy" else bid
            
            # Monitor and calculate the profit of a position
            
            pos["profit"] = self._calculate_profit(action=pos["type"], symbol=pos["symbol"], lotsize=pos["volume"], entry_price=pos["open_price"], 
                                                    exit_price=(ask if pos["type"]=="buy" else bid))

(b): Мониторинг выходов из позиций

После того как позиция открыта в тестере стратегий, с заданными целями (stop loss и take profit) или без них, она не закроется сама.

Мы должны постоянно отслеживать ее, проверяя, достигла ли текущая рыночная цена (ask для позиции sell и bid для позиции buy) такой желаемой цели. Если она достигла одной из целей позиции, такая позиция закрывается, а сделка добавляется в историю сделок.

    def monitor_positions(self, verbose: bool):
        
        # monitoring all open trades
        
        for pos in self.positions_container:
                
            self.m_symbol.name(pos["symbol"])
            self.m_symbol.refresh_rates()
            
            # Get ticks information for every symbol
            
            ask = self.m_symbol.ask()
            bid = self.m_symbol.bid()
            

            # ... other monitors

            
            # Monitor the stoploss and takeprofit situation of positions
            
            if pos["tp"] > 0 and ((pos["type"] == "buy" and bid >= pos["tp"]) or (pos["type"] == "sell" and ask <= pos["tp"])): # Take profit hit    
                self.position_close(pos_id=pos) # close such position
                
            if pos["sl"] > 0 and ((pos["type"] == "buy" and bid <= pos["sl"]) or (pos["type"] == "sell" and ask >= pos["sl"])): # Stop loss hit
                self.position_close(pos_id=pos) # close such position

Наконец, мы хотим выводить некоторую информацию о каждой позиции по мере ее обновления, аналогично тому, как это делает панель инструментов терминала MetaTrader 5 (она показывает активные позиции).

Только когда переменная с именем verbose = True.

            # Print the information about all trades (positions and orders (if any))            
            
            if verbose:
                print(f'sim -> ticket | {trade["id"]} | symbol {trade["symbol"]} | time {trade["time"]} | type {trade["type"]} | volume {trade["volume"]} | sl {trade["sl"]} | tp {trade["tp"]} | profit {trade["profit"]:.2f}')

Пока мы отслеживаем только позиции buy и sell; чуть позже мы также обсудим мониторинг отложенных ордеров.


Отложенные ордера

В отличие от рыночных ордеров (позиций), предназначенных для немедленного исполнения по рынку, отложенные ордера содержат приказ выполнить торговую операцию при наличии определенного условия. Отложенные ордера также могут содержать ограничение по времени действия — дату истечения ордера.

К отложенным ордерам относятся.

  1. Buy Limit
  2. Buy Stop
  3. Sell Limit
  4. Sell Stop
  5. Buy Stop Limit
  6. Sell Stop Limit

Пока мы реализуем первые четыре типа отложенных ордеров из списка выше в классе торгового симулятора, просто чтобы начать.

Начнем с базовой функции для размещения отложенных ордеров.

Проверки:

(a): Проверка правильности типа ордера.

    def _place_a_pending_order(self, 
                               order_type: str,
                               volume: float,
                               symbol: str,
                               open_price: float,
                               sl: float = 0.0,
                               tp: float = 0.0,
                               comment: str = "",
                               expiry_date: datetime = None,
                               expiration_mode: str="gtc"
                               ):
        
        order_types = ["buy limit", "buy stop", "sell limit", "sell stop"]
        
        if order_type not in order_types:
            raise ValueError(f"Invalid pending order type, available order types include: {order_types}")
        
        expiration_modes = ["gtc", "daily", "daily_excluding_stops"]
        if expiration_mode not in expiration_modes:
            raise ValueError(f"Invalid Expiration mode, available modes include: {expiration_modes}")
        

(b): Проверка, что все отложенные ордера не слишком близки к рынку

  1. Проверка, что цена открытия отложенного ордера, связанного с buy, не слишком близка к цене bid.
  2. Проверка, что цена открытия отложенного ордера, связанного с sell, не слишком близка к цене ask.
Значение SYMBOL_TRADE_STOPS_LEVEL  определяет, насколько близко позиция находится к рынку.

# Get market info
        
self.m_symbol.name(symbol_name=symbol) # assign symbol's name
self.m_symbol.refresh_rates() # get recent ticks from the market using the current selected symbol
        
if order_type in ("buy limit", "buy stop"):
            
    if abs(open_price - self.m_symbol.bid()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
        
if order_type in ("sell limit", "sell stop"):
            
    if abs(open_price - self.m_symbol.ask()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")

(c): Проверка получения допустимой даты истечения ордера

Дата или время истечения должны быть значением времени больше текущего времени — временем в будущем.

# check if the order has a valid expiry date
        
if expiry_date is not None: # if an expiry date is given in the first place
    if expiry_date <= self.m_symbol.time(timezone=pytz.UTC):
        print(f"Failed to place a pending order {order_type}, Invalid datetime")
        return

Наконец, после прохождения трех проверок ордер добавляется в список ордеров, хранящийся в классе.

order_info = self.order_info.copy()
        
self.id += 1
        
order_info["id"] = self.id
order_info["type"] = order_type
order_info["volume"] = volume
order_info["symbol"] = symbol
order_info["open_price"] = open_price
order_info["sl"] = sl
order_info["tp"] = tp
order_info["comment"] = comment
order_info["magic"] = self.magic_number
order_info["margin_required"] = self._calculate_margin(symbol=symbol, volume=volume, open_price=open_price)
        
order_info["expiry_date"] = expiry_date
order_info["expiration_mode"] = expiration_mode
        
self.orders_container.append(order_info) # add a valid order to it's container

Мы увеличиваем тот же id (номер тикета), который используется при установке id позиций, также и при размещении отложенных ордеров, потому что отложенный ордер — это замаскированная позиция, (это позиция, ожидающая открытия, а каждая позиция когда-то была ордером).

Использование того же id помогает избежать дублирования идентификаторов  в сработавших позициях.

На основе этой базовой функции реализуем отдельные удобные методы для размещения отложенных ордеров.

Размещение ордера Buy Stop:

    def buy_stop(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None,expiration_mode: str="gtc"):
        
        # validate an order according to it's type
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()
        
        if self.m_symbol.bid() >= open_price:
            print("Failed to place a buy stop order, open price <= the bid price")    
            return
        
        self._place_a_pending_order("buy stop", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)    

Размещение ордера Buy Limit:

    def buy_limit(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"):
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()
        
        if self.m_symbol.bid() <= open_price:
            print("Failed to place a buy limit order, open price >= current bid price")
            return

        self._place_a_pending_order("buy limit", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)

Размещение ордера Sell Stop:

    def sell_stop(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"):
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()

        if self.m_symbol.ask() <= open_price:
            print("Failed to place a sell stop order, open price >= current ask price")
            return

        self._place_a_pending_order("sell stop", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)

Размещение ордера Sell Limit:

    def sell_limit(self, volume: float, symbol: str, open_price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "", expiry_date: datetime = None, expiration_mode: str="gtc"):
        
        self.m_symbol.name(symbol_name=symbol)
        self.m_symbol.refresh_rates()

        if self.m_symbol.ask() >= open_price:
            print("Failed to place a sell limit order, open price <= current ask price")
            return

        self._place_a_pending_order("sell limit", volume, symbol, open_price, sl, tp, comment, expiry_date, expiration_mode)

В функциях выше мы добавляем условия, чтобы каждый ордер размещался хотя бы в правильном месте, т. е.

  1. Ордер buy stop размещается выше текущей рыночной цены (цены ask)
  2. Ордер buy limit размещается ниже текущей рыночной цены (цены bid)
  3. Ордер sell stop размещается ниже текущей рыночной цены (цены bid)
  4. Ордер sell limit размещается выше текущей рыночной цены (цены ask) 


Удаление отложенных ордеров

Наличие функции, отвечающей за удаление отложенных ордеров, так же важно, как и наличие функции для их размещения.

В этой функции проверки не нужны, и после удаления ордера сделка не сохраняется.

    def order_delete(self, selected_order: dict) -> bool:
        
        # delete a pending order from the orders container
        
        if selected_order in self.orders_container:
            
            self.orders_container.remove(selected_order)
            return True
        
        else:
            print(f"Warning: An Order with ID {selected_order['id']} not found!")
            return False


Изменение отложенных ордеров

Нам также нужна функция для изменения отложенных ордеров, аналогично функции изменения позиций.

В функции для этой задачи требуются три важные проверки.

(a): Проверка, что новая цена открытия позиции находится в правильном месте в соответствии с типом ордера.

Во всех отложенных ордерах новая цена открытия должна быть размещена:

  1. Выше текущей рыночной цены (цены ask) для ордера buy stop
  2. Ниже текущей рыночной цены (цены bid) для ордера buy limit
  3. Ниже текущей рыночной цены (цены bid) для ордера sell stop
  4. Выше текущей рыночной цены (цены ask) для ордера sell limit
    def order_modify(self, order: dict, new_open_price: float, new_sl: float, new_tp: float, new_expiry: datetime = None, new_expiration_mode: str = None):
        """
         Modify an existing pending order's open price, SL/TP, and optionally its expiration settings.
        """
        new_order = order.copy()

        # Validate order type
        valid_types = ["buy limit", "buy stop", "sell limit", "sell stop"]
        if order["type"] not in valid_types:
            print(f"Invalid order type for modification: {order['type']}")
            return False

        self.m_symbol.name(order["symbol"])
        self.m_symbol.refresh_rates()

        # Ensure open price is placed logically according to type
        ask = self.m_symbol.ask()
        bid = self.m_symbol.bid()

        if order["type"] == "buy stop" and bid >= new_open_price:
            print("Failed to modify Buy Stop: new open price <= current bid price")
            return False
        if order["type"] == "buy limit" and bid <= new_open_price:
            print("Failed to modify Buy Limit: new open price >= current bid price")
            return False
        if order["type"] == "sell stop" and ask <= new_open_price:
            print("Failed to modify Sell Stop: new open price >= current ask price")
            return False
        if order["type"] == "sell limit" and ask >= new_open_price:
            print("Failed to modify Sell Limit: new open price <= current ask price")
            return False

(b): Проверка, что новая цена открытия ордера не слишком близка к рынку.

# ensure the order ins't close to the market
        
order_type = order["type"]
if order_type in ("buy limit", "buy stop"):
            
    if abs(new_open_price - self.m_symbol.bid()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
        return False
        
if order_type in ("sell limit", "sell stop"):
            
    if abs(new_open_price - self.m_symbol.ask()) < self.m_symbol.stops_level() * self.m_symbol.point():
        print(f"Failed to open a pending order, a '{order_type}' order is too close to the market")
        return False

(c): Проверка корректности нового времени истечения ордера

if new_expiry and new_expiry <= self.m_symbol.time(timezone=pytz.UTC):
    print("Invalid Expiry date, new expiry date must be a value in the future")

Наконец, мы изменяем и обновляем все ордера в контейнере.

# Update the order in the container
for i, o in enumerate(self.orders_container):
    if o["id"] == order["id"]:
        self.orders_container[i] = new_order
        print(f"Order with id=[{order['id']}] modified successfully.")
        return True

print("Failed to modify order: ID not found")
return False


Мониторинг отложенных ордеров

Как и позиция, ордер — это всего лишь набор информации, хранящийся в списке словарей в классе. После сохранения ордера его нужно постоянно отслеживать, то есть должен быть код, проверяющий, достигла ли текущая цена (ask или bid) цены открытия отложенного ордера; когда текущая рыночная цена достигает цены открытия ордера, он срабатывает и вместо этого добавляется в список открытых позиций.

Также нужно отслеживать ситуацию со временем истечения у всех отложенных ордеров с датой истечения и правильным режимом истечения, подробнее.

    def monitor_pending_orders(self):
        
        now = datetime.now(tz=pytz.UTC)
        
        expired_orders = []
        triggered_orders = []

        for order in self.orders_container: # loop through all orders
            
            expiration_mode = order.get("expiration_mode", "gtc")
            expiry_date = order.get("expiry_date")

            # Check for expiration based on mode
            if expiration_mode == "daily" or expiration_mode == "daily_excluding_stops":
                if expiry_date and now >= expiry_date:
                    
                    expired_orders.append(order)
                    continue  # Skip to next order

            self.m_symbol.name(symbol_name=order["symbol"])
            
            if not self.m_symbol.refresh_rates():
                continue

            ask = self.m_symbol.ask()
            bid = self.m_symbol.bid()
            open_price = order["open_price"]
            order_type = order["type"].lower()
            
            if order_type in ("buy limit", "buy stop"):
                order["price"] = self.m_symbol.ask()

            if order_type in ("sell limit", "sell stop"):
                order["price"] = self.m_symbol.bid()
                
            triggered = False # store the triggered condition of an order
            
            if order_type == "buy limit" and ask <= open_price:
                triggered = self.buy(order["volume"], order["symbol"], ask, order["sl"], order["tp"], order["comment"]) # open a buy position with credentials taken from an order

            elif order_type == "buy stop" and ask >= open_price:
                triggered = self.buy(order["volume"], order["symbol"], ask, order["sl"], order["tp"], order["comment"]) # open a buy position

            elif order_type == "sell limit" and bid >= open_price:
                triggered = self.sell(order["volume"], order["symbol"], bid, order["sl"], order["tp"], order["comment"]) # open a sell position

            elif order_type == "sell stop" and bid <= open_price:
                triggered = self.sell(order["volume"], order["symbol"], bid, order["sl"], order["tp"], order["comment"]) # open a sell position

            if triggered:
                triggered_orders.append(order) # add a triggerd order to the list 

        # Clean up expired and triggered orders
        for order in expired_orders + triggered_orders:
            
            if order in self.orders_container:
                self.orders_container.remove(order)


Мониторинг счета

После мониторинга всех позиций и обновления их параметров (включая значения прибыли/убытка) нам также нужно обновить параметры счета: баланс счета согласно депозиту симулятора, equity, margin, free margin и margin level — все эти параметры счета зависят от торговой активности.

Имитируемый счет отслеживается в функции с именем monitor_account:

Свойство счета
Расчет

Описание
Расчет текущей прибыли/убытка
unrealized_pl = sum(pos['profit'] or 0 for pos in self.positions_container)
        
self.account_info["profit"] = unrealized_pl
Рассчитывает сумму прибыли от всех открытых позиций в симуляторе.
Обновление equity счета
self.account_info['equity'] = self.account_info['balance'] + unrealized_pl
Equity счета — это результат суммы прибыли\убытков по всем позициям при добавлении к балансу счета.
Использованная маржа
self.account_info['margin'] = sum(pos['margin_required'] or 0 for pos in self.positions_container)
Общая использованная маржа — это сумма маржи, потребленной всеми позициями.
Свободная маржа
self.account_info['free_margin'] = self.account_info['equity'] - self.account_info['margin']
Свободная маржа — это разница между equity счета и общей маржей, использованной на счете.
Уровень маржи
self.account_info['margin_level'] = (self.account_info['equity'] / self.account_info['margin']) * 100 \
            if self.account_info['margin'] > 0 else 0.0
Равен equity счета, деленному на маржу счета в процентах, только когда использованная маржа больше нуля (margin > 0).

Наконец, мы выводим параметры счета в конце функции с именем monitor_account.

Только когда аргумент с именем verbose = True.

    def monitor_account(self, verbose: bool):
        
        """Recalculates all account metrics based on current positions"""
        
        # 1. Calculate unrealized P/L
        unrealized_pl = sum(pos['profit'] or 0 for pos in self.open_trades_container)
        
        self.account_info["profit"] = unrealized_pl
        
        # 2. Update Equity (Balance + Floating P/L)
        self.account_info['equity'] = self.account_info['balance'] + unrealized_pl
        
        # 3. Calculate Used Margin
        self.account_info['margin'] = sum(pos['margin_required'] or 0 for pos in self.open_trades_container)
        
        # 4. Calculate Free Margin (Equity - Used Margin)
        self.account_info['free_margin'] = self.account_info['equity'] - self.account_info['margin']
        
        # 5. Calculate Margin Level (Equity / Margin * 100)
        self.account_info['margin_level'] = (self.account_info['equity'] / self.account_info['margin']) * 100 \
            if self.account_info['margin'] > 0 else 0.0
        
        if verbose:
            print(f"Balance: {self.account_info['balance']:.2f} | Equity: {self.account_info['equity']:.2f} | Profit: {self.account_info['profit']:.2f} | Margin: {self.account_info['margin']:.2f} | Free margin: {self.account_info['free_margin']} | Margin level: {self.account_info['margin_level']:.2f}%")

Баланс счета обновляется только при закрытии сделки; это возвращает нас к функции с именем position_close.

    def position_close(self, selected_pos: dict) -> bool:

        # Update deal info
        
        deal_info = selected_pos.copy()
        deal_info["direction"] = "closed"
        
        # check if the reason was SL or TP according to recent tick/price information
        
        self.m_symbol.name(selected_pos["symbol"])
        self.m_symbol.refresh_rates()
        
        ask = self.m_symbol.ask()
        bid = self.m_symbol.bid()
        digits = self.m_symbol.digits()
        
        deal_info["reason"] = "Unknown" # Unkown deal reason if the stoploss or takeprofit wasn't hit
        
        if selected_pos["type"] == "buy":
            if np.isclose(selected_pos["tp"], bid, digits): # check if the current bid price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], bid, digits): # check if the current bid price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"           
        
        
        if selected_pos["type"] == "sell":
            if np.isclose(selected_pos["tp"], ask, digits): # check if the current ask price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], ask, digits): # check if the current ask price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"               
        
        
        self.deals_container.append(deal_info.copy()) # add the deal to the deals container
        
        print("Trade closed successfully: ", deal_info)
        
        # Save closed deal to database
        self._save_closed_deal(deal_info, self.history_db_name)
        
        # Remove trade from open positions
        
        if selected_pos in self.open_trades_container:
                
            # update the account balance
            self.account_info["balance"] += selected_pos["profit"]
            
            self.open_trades_container.remove(selected_pos)
        else:
            print(f"Warning: Position with ID {selected_pos['id']} not found!")

        return True


Симуляция торговли в реальном времени на Python

Имея возможность открывать сделки и отслеживать торговую активность в классе, TradeSimulator, давайте откроем наши первые сделки в симуляции, а также реальные сделки в настольном приложении MetaTrader 5. Цель — найти сходства между торговой активностью в двух разных средах.

Перед открытием сделок нужно помнить о методах, используемых для настройки важных торговых параметров в симуляторе.

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

    #... other functions

    def set_magicnumber(self, magic_number: int):
        
        self.magic_number = magic_number
        
    def set_deviation_in_points(self, deviation_points: int):
        
        self.deviation_points = deviation_points

Функция с именем set_magicnumber  задает magic number для всех сделок в симуляторе, а функция с именем set_deviation_in_points  задает проскальзывание всех сделок в классе. 

После импорта всех необходимых модулей внутри файла simulator_test.py, мы инициализируем настольное приложение MetaTrader 5 с помощью модуля MetaTrader5.

import MetaTrader5 as mt5
from Trade.SymbolInfo import CSymbolInfo
from Trade.Trade import CTrade
from datetime import datetime
import time
import pytz
from trade_simulator import TradeSimulator


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

Затем инициализируем TradeSimulator класс.

sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")

magic_number = 123456
slippage = 10

sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator
sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator

Мы будем использовать класс CTrade рассмотренный в этой статье для открытия тех же сделок в MetaTrader 5, мы сравним сделки, открытые в симуляторе, со сделками, открытыми в MetaTrader 5.

m_trade = CTrade() # Initializing the CTrade class

symbol = "EURUSD"

m_trade.set_magicnumber(magic_number=magic_number) # sets the magic number of the CTrade class
m_trade.set_deviation_in_points(deviation_points=slippage) # sets slippage
m_trade.set_filling_type_by_symbol(symbol=symbol) #set filling type by the given symbol

Мы открываем одинаковые сделки как в торговом симуляторе, так и в MetaTrader 5.

m_symbol = CSymbolInfo(mt5_instance=mt5)
m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo

if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader 5
    print("failed to get recent ticks data")

sim.monitor_account(verbose=True)  # calculate account credentials initially

# Open trades in a Simulator

lotsize = 0.01

if not sim.buy(volume=lotsize, symbol=symbol, open_price=m_symbol.ask(), sl=0.0, tp=0.0, comment="Test Buy Trade"):
    print("Failed to simulate a trade")

if not sim.sell(volume=lotsize, symbol=symbol, open_price=m_symbol.bid(), sl=0.0, tp=0.0, comment="Test Sell Trade"):
    print("Failed to simulate a trade")

# Open trades in MetaTrader5 

if not m_trade.buy(volume=lotsize, symbol=symbol, price=m_symbol.ask(), sl=0.0, tp=0.0, comment="Test Buy Trade"):
    print("Failed to open a trade in MetaTrader5")
    
if not m_trade.sell(volume=lotsize, symbol=symbol, price=m_symbol.bid(), sl=0.0, tp=0.0, comment="Test Buy Trade"):
    print("Failed to open a trade in MetaTrader5")

Внутри бесконечного цикла мы хотим отслеживать все позиции и счет в симуляции.

while True: # constantly monitor trades and account metrics
        
    sim.monitor_account(verbose=True)
    sim.monitor_positions(verbose=True)
    
    time.sleep(1) # sleep for one second

Вывод.

Наблюдать за этим не очень приятно. Давайте создадим простое GUI-приложение, которое поможет визуализировать эту торговую активность в Python.


GUI-приложение для моделирования в реальном времени

Для этого простого приложения мы используем модуль tkinter.

import tkinter as tk
from tkinter import ttk
from datetime import datetime

class SimToolboxGUI:

    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Trade Simulator Monitor")
        self.root.geometry("900x700")
        self.root.configure(bg="#f0f0f0")

        # === ACCOUNT INFO DISPLAY ===
        self.account_label = tk.Label(
            self.root,
            text="",
            font=("Courier", 8),
            anchor="w",
            justify="left",
            bg="#f0f0f0",
            fg="#333",
        )
        self.account_label.pack(fill="x", padx=5, pady=(5, 6))

        # === POSITION TABLE ===
        position_frame = tk.LabelFrame(self.root, text="Open Positions", bg="#f0f0f0")
        position_frame.pack(fill="both", expand=True, padx=10, pady=5)

        self.position_columns = [
            "id", "symbol", "time", "type", "volume", "open_price", "sl", "tp",
            "swap", "price", "profit", "comment"
        ]

        self.position_tree = ttk.Treeview(position_frame, columns=self.position_columns, show="headings", height=10)
        for col in self.position_columns:
            self.position_tree.heading(col, text=col)
            self.position_tree.column(col, anchor="center", width=80)
        self.position_tree.pack(fill="both", expand=True, padx=5, pady=5)

        vsb1 = ttk.Scrollbar(position_frame, orient="vertical", command=self.position_tree.yview)
        self.position_tree.configure(yscrollcommand=vsb1.set)
        vsb1.pack(side="right", fill="y")

        # === ORDER TABLE ===
        order_frame = tk.LabelFrame(self.root, text="Pending Orders", bg="#f0f0f0")
        order_frame.pack(fill="both", expand=True, padx=10, pady=5)

        self.order_columns = [
            "id", "symbol", "time", "type", "volume", "open_price", "sl", "tp", "price",
            "expiry_date", "expiration_mode", "comment"
        ]

        self.order_tree = ttk.Treeview(order_frame, columns=self.order_columns, show="headings", height=10)
        for col in self.order_columns:
            self.order_tree.heading(col, text=col)
            self.order_tree.column(col, anchor="center", width=100)
        self.order_tree.pack(fill="both", expand=True, padx=5, pady=5)

        vsb2 = ttk.Scrollbar(order_frame, orient="vertical", command=self.order_tree.yview)
        self.order_tree.configure(yscrollcommand=vsb2.set)
        vsb2.pack(side="right", fill="y")

    def update(self, account_info: dict, positions: list, orders: list):
        # === Update account info ===
        acc_text = (
            f"Balance: {account_info['balance']:.2f} | "
            f"Equity: {account_info['equity']:.2f} | "
            f"Profit: {account_info['profit']:.2f} | "
            f"Margin: {account_info['margin']:.2f} | "
            f"Free margin: {account_info['free_margin']:.5f} | "
            f"Margin level: {account_info['margin_level']:.2f}%"
        )
        self.account_label.config(text=acc_text)

        # === Refresh positions ===
        for row in self.position_tree.get_children():
            self.position_tree.delete(row)

        for pos in positions:
            row = [pos.get(col, "") for col in self.position_columns]
            self.position_tree.insert("", "end", values=row)

        # === Refresh orders ===
        for row in self.order_tree.get_children():
            self.order_tree.delete(row)

        for order in orders:
            row = []
            for col in self.order_columns:
                val = order.get(col, "")
                if isinstance(val, datetime):
                    val = val.strftime("%Y-%m-%d %H:%M:%S")
                row.append(val)
            self.order_tree.insert("", "end", values=row)

        self.root.update()

    def run(self):
        self.root.mainloop()

Класс выше создает две таблицы: одну для отображения ордеров, другую — для позиций. В верхней части GUI мы добавляем информацию о счете.

В классе TradeSimulator, мы инициализируем этот Simulation ToolBox GUI в конструкторе класса.

Внутри trade_simulator.py

from toolbox_gui import SimToolboxGUI

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

        # ... other variables

        self.toolbox_gui = SimToolboxGUI()  # Initialize the GUI

Мы создаем отдельную функцию для обновления данных, отображаемых в GUI-приложении.

class TradeSimulator:
    
    # ... other functions               

    def run_toolbox_gui(self):
        
        """
        Runs the simulator toolbox GUI.
        """
        
        self.toolbox_gui.update(self.account_info, self.open_trades_container)

После вызова функций для мониторинга и регулирования позиций, ордеров и счета мы вызываем функцию обновления GUI-приложения.

while True: # constantly monitor trades and account metrics
    
    sim.monitor_account(verbose=False)
    sim.monitor_positions(verbose=False)
    sim.monitor_orders()
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    time.sleep(1) # sleep for one second

Снова откроем несколько позиций и ордеров как в MetaTrader 5, так и в Python-симуляторе, а затем посмотрим на результаты в обоих.

Имя файла: simulator_test.py

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


sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")

magic_number = 123456
slippage = 10

sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator
sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator

m_trade = CTrade() # Initializing the CTrade class

symbol = "EURUSD"

m_trade.set_magicnumber(magic_number=magic_number) # sets the magic number of the CTrade class
m_trade.set_deviation_in_points(deviation_points=slippage) # sets slippage
m_trade.set_filling_type_by_symbol(symbol=symbol) #set filling type by the given symbol

m_symbol = CSymbolInfo(mt5_instance=mt5)
m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo


# Open trades in a Simulator

sim.monitor_account(verbose=False)

if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5
    print("failed to get recent ticks data")
    
# Market Orders

sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())

m_trade.buy(volume=0.1, symbol=symbol, price=m_symbol.ask())
m_trade.sell(volume=0.1, symbol=symbol, price=m_symbol.bid())

# Pending Orders

expiry = datetime.now(tz=pytz.UTC) + timedelta(days=1) # expiration date for pending orders
price_gap = 0.0005

# Buy Stop: place above current ask
sim.buy_stop(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap, sl=0.0, tp=0.0,
             comment="Buy Stop Example", expiry_date=expiry, expiration_mode="daily")

m_trade.buy_stop(volume=0.1, symbol=symbol, price=m_symbol.ask() + price_gap)

# Buy Limit: place below current bid
sim.buy_limit(volume=0.1, symbol=symbol, open_price=m_symbol.bid() - price_gap, sl=0.0, tp=0.0,
              comment="Buy Limit Example", expiry_date=expiry, expiration_mode="daily_excluding_stops")

m_trade.buy_limit(volume=0.1, symbol=symbol, price=m_symbol.bid() - price_gap)

# Sell Stop: place below current bid

sim.sell_stop(volume=0.1, symbol=symbol, open_price=m_symbol.bid() - price_gap, sl=0.0, tp=0.0,
              comment="Sell Stop Example", expiry_date=expiry, expiration_mode="gtc")

m_trade.sell_stop(volume=0.1, symbol=symbol, price=m_symbol.ask() - price_gap)

# Sell Limit: place above current ask
sim.sell_limit(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap, sl=0.0, tp=0.0,
               comment="Sell Limit Example", expiry_date=expiry, expiration_mode="gtc")

m_trade.sell_limit(volume=0.1, symbol=symbol, price=m_symbol.bid() + price_gap)

while True: # constantly monitor trades and account metrics
    
    sim.monitor_account()
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    sim.monitor_orders()
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    time.sleep(1) # sleep for one second

Вывод.

Результаты симуляции пока не полностью совпадают с реальной торговлей, но и не слишком сильно от нее отличаются — а это уже хороший прогресс.


Внешнее управление и контроль позиций и ордеров

Возможность получать информацию об открытых позициях и управлять ими вне симулятора очень важна, именно в этом и состоит алгоритмическая торговля.

Например, многие торговые стратегии требуют знания ранее открытых позиций. Например, стратегия может требовать, чтобы робот открывал позицию buy только в том случае, если позиция в этом направлении и по этому инструменту еще не существует.

Итак, ниже приведена таблица с функциями, которые позволяют получать доступ ко всем ордерам, позициям и сделкам вне класса с именем TradeSimulator.

ФункцияВозвращает
def get_positions(self) -> list:
Возвращает все открытые позиции из контейнера.
def get_orders(self) -> list:
Возвращает все открытые ордера из контейнера.
def get_deals(self, start_time: datetime = None, end_time: datetime = None, from_db: bool = False) -> list
Возвращает все сделки, исполненные между определенным временным интервалом, заданным двумя переменными (start_time и end_time). 
Необязательная переменная с именем from_db, помогает выбрать между сделками, временно сохраненными в памяти, и сделками из базы данных.

Пример использования:

sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())

price_gap = 0.0005
# Buy Stop: place above current ask
sim.buy_stop(volume=0.1, symbol=symbol, open_price=m_symbol.ask() + price_gap)


print("Positions total: ",len(sim.get_positions()))
print("Orders total: ",len(sim.get_orders()))

now = m_symbol.time(timezone=pytz.UTC)
start_time = now - timedelta(minutes=5)
end_time = now

print("Deals total: ",len(sim.get_deals(start_time=start_time,
                                        end_time=end_time,
                                        from_db=False
                              )))

Вывод.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy 
Tester/simulator_test.py"
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 9, 59, 51, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14597, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 9, 59, 51, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14589, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Margin calculation mode:   Calculation of profit and margin for Forex
Positions total:  2
Orders total:  1
Deals total:  2

При выборе сделок следует использовать время символа в UTC — то же, что использовалось при открытии позиций и ордеров, а не текущее локальное время, чтобы избежать расхождений во времени.

Затем эти функции позволят нам вводить конкретные условия в наши торговые стратегии.

(a): Проверка существования определенного типа сделки в симуляции

Это очень часто используется для мониторинга сделок. В некоторых торговых стратегиях мы часто хотим открывать определенные позиции и ордера только тогда, когда они не существуют.

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

sim = TradeSimulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")

magic_number = 123456
slippage = 10

sim.set_magicnumber(magic_number=magic_number) #sets the magic number of a simulator
sim.set_deviation_in_points(deviation_points=slippage) # sets slippage of the simulator

symbol = "EURUSD"
m_symbol = CSymbolInfo(mt5_instance=mt5)
m_symbol.name(symbol_name=symbol) # sets the symbol name for the class CSymbolInfo


def is_position_exists(type: str) -> bool:
    
    for pos in sim.get_positions():
        if pos["magic"] == magic_number and pos["symbol"] == symbol and pos["type"] == type:
            return True # position exists
        
    return False
    
while True: #imitating the OnTick function offered in MQL5 language
    
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    sim.monitor_account(verbose=False)
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5
        # print("failed to get recent ticks data")
        continue
        
    if not is_position_exists("buy"): # open a buy trade in a simulator if it doesn't exist
        sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
    
    if not is_position_exists("sell"): # open a sell trade in a simulator if it doesn't exist
        sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())
    
    time.sleep(1) # sleep for one second    

Вывод.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy 
Tester/simulator_test.py"
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 13, 18, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14565, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 13, 18, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14557, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}

Были открыты только две разные позиции (позиции buy и sell).

Это знакомый интерфейс, похожий на тот, который предлагается в MQL5 и который мы часто используем для проверки существования определенной позиции.

(b): Закрытие конкретной позиции

def close_positions(type: str):
    
    for pos in sim.get_positions():
        if pos["magic"] == magic_number and pos["symbol"] == symbol and pos["type"] == type:
            sim.position_close(pos)

Некоторым стратегиям может потребоваться закрывать определенные сделки при выполнении конкретного запрограммированного условия; в этом смысле приведенная выше функция или похожий подход становится полезным.

Давайте откроем две позиции (buy и sell) и закроем позицию buy.

while True:
    
    sim.monitor_pending_orders()
    sim.monitor_positions(verbose=False)
    sim.monitor_account(verbose=False)
    
    sim.run_toolbox_gui()  # Run the simulator toolbox GUI
    
    if m_symbol.refresh_rates() is None: # Get recent ticks data from MetaTrader5
        # print("failed to get recent ticks data")
        continue
        
    if not is_position_exists("buy"): # open a buy trade in a simulator if it doesn't exist
        sim.buy(volume=0.1, symbol=symbol, open_price=m_symbol.ask())
    
    close_positions("buy") # close all buy positions
    
    if not is_position_exists("sell"): # open a sell trade in a simulator if it doesn't exist
        sim.sell(volume=0.1, symbol=symbol, open_price=m_symbol.bid())
    
    time.sleep(1) # sleep for one second    

Вывод.

(pystrategytester) C:\Users\Omega Joctan\OneDrive\Desktop\Python Strategy Tester>conda run --live-stream --name pystrategytester python "c:/Users/Omega Joctan/OneDrive/Desktop/Python Strategy 
Tester/simulator_test.py"
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14447, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade closed successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 1, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14447, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': '', 'direction': 'closed', 'reason': 'Take profit'}        
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 35, tzinfo=<UTC>), 'id': 2, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'sell', 'volume': 0.1, 'open_price': 1.14439, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade opened successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 37, tzinfo=<UTC>), 'id': 3, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14446, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': ''}
Trade closed successfully:  {'time': datetime.datetime(2025, 7, 31, 10, 50, 37, tzinfo=<UTC>), 'id': 3, 'magic': 123456, 'symbol': 'EURUSD', 'type': 'buy', 'volume': 0.1, 'open_price': 1.14446, 'price': 0.0, 'sl': 0.0, 'tp': 0.0, 'commission': 0.0, 'margin_required': 20.0, 'fee': 0.0, 'swap': 0.0, 'profit': 0.0, 'comment': '', 'direction': 'closed', 'reason': 'Take profit'}        


Работа со сделками

В MetaTrader 5 сделка представляет фактическое исполнение торговой операции — это результат ордера. Каждая сделка основана на конкретном ордере, но один ордер может создавать несколько сделок (например, если ордер исполняется частями).

Сделки создаются, когда.

  1. Позиция открывается,
  2. Позиция частично или полностью закрывается,
  3. Или ордер (например limit или stop order) срабатывает и исполняется.

Другими словами, как входы, так и выходы записываются как сделки.

В отличие от ордеров и позиций, которые можно временно изменять, сделки неизменяемы и всегда сохраняются в торговой истории. Они служат постоянной записью исполненных сделок и не могут быть изменены или удалены.

В конце обеих функций _position_open и position_close  которые соответственно открывают и закрывают позиции, сведения о сделке добавляются в список с именем deals_container находящийся в конструкторе класса.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        # ... other operations
        # ...

        # Append to open trades
        self.open_trades_container.append(trade_info)
        print("Trade opened successfully: ", trade_info)

        # Track deal
        self.deal_info.update(trade_info)
        self.deal_info["direction"] = "opened"
        self.deal_info["reason"] = "Expert"
        self.deals_container.append(self.deal_info.copy())
    def position_close(self, selected_pos: dict) -> bool:

        # Update deal info
        
        deal_info = selected_pos.copy()
        deal_info["direction"] = "closed"
        
        # ... other operations
        
        deal_info["reason"] = "Unknown" # Unkown deal reason if the stoploss or takeprofit wasn't hit
        
        if selected_pos["type"] == "buy":
            if np.isclose(selected_pos["tp"], bid, digits): # check if the current bid price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], bid, digits): # check if the current bid price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"           
        
        
        if selected_pos["type"] == "sell":
            if np.isclose(selected_pos["tp"], ask, digits): # check if the current ask price is almost equal to the takeprofit
                deal_info["reason"] = "Take profit"           
                
            elif np.isclose(selected_pos["sl"], ask, digits): # check if the current ask price is almost equal to the stoploss
                deal_info["reason"] = "Stop loss"               
        
        
        self.deals_container.append(deal_info.copy()) # add the deal to the deals container
        
        print("Trade closed successfully: ", deal_info)

Однако хранить сделки, открытые симулятором, в списке/массиве не идеально, потому что эта информация будет потеряна, как только программа закроется. Давайте сохраним их в базе данных SQLite3 и сделаем запись постоянной, если только она не будет изменена или удалена, аналогично тому, как это делает MetaTrader 5.

    def _create_deals_db(self, db_name: str):
        
        """
         Creates a SQLite database to store trade history and account information.
        
        Args:
            db_name (str): The name of the database file.
        """
        
        conn = sqlite3.connect(db_name)
        cursor = conn.cursor()

        # Create tables if they do not exist
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS closed_deals (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                time TEXT,
                magic INTEGER,
                symbol TEXT,
                type TEXT,
                direction TEXT,
                volume REAL,
                price REAL,
                sl REAL,
                tp REAL,
                commission REAL,
                margin_required REAL,
                fee REAL,
                swap REAL,
                profit REAL,
                comment TEXT,
                reason TEXT
            )
        ''')
        
        conn.commit() 
        conn.close()

Функция выше вызывается в классе TradeSimulator, в конструкторе.

class TradeSimulator:
    def __init__(self, simulator_name: str, mt5_instance: mt5, deposit: float, leverage: str="1:100"):
        
        # ... other variables
        # ...

        # Database for trade history
        
        self.sim_folder = "Simulations"
        
        os.makedirs(self.sim_folder, exist_ok=True)  # Ensure the simulations path exists
        
        # Create the database file name
        
        self.history_db_name = os.path.join(self.sim_folder, self.simulator_name+".db")
        self._create_deals_db(self.history_db_name)

После создания базы данных, похожей на имя симулятора, заданное переменной simulator_name,  функция с именем _create_deals_db создает таблицу с именем closed_deals если она не существует.

Нам также нужна функция для сохранения каждой отдельной сделки в базу данных.

    def _save_deal(self, deal: dict, db_name: str):
        """
            Saves a closed deal to the SQLite database.
        """
        
        conn = sqlite3.connect(db_name)
        cursor = conn.cursor()

        cursor.execute("""
            INSERT INTO closed_deals (
                time, magic, symbol, type, direction, volume, price, sl, tp,
                commission, margin_required, fee, swap, profit, comment, reason
            ) VALUES (
                :time, :magic, :symbol, :type, :direction, :volume, :price, :sl, :tp,
                :commission, :margin_required, :fee, :swap, :profit, :comment, :reason
            );
        """, deal)

        conn.commit()
        conn.close()

Обратите внимание, что мы не добавляем столбец с именем id в базу данных? Это потому, что столбец id в таблице базы данных установлен как AUTOINCREMENT чтобы каждая сделка получала уникальный id на протяжении всей истории от 0 до положительной бесконечности.

Мы должны сохранять все сделки в базу данных внутри функций, отвечающих за открытие и закрытие позиций, после сохранения их в список с именем deals_container.

В функции с именем position_close.

    def position_close(self, selected_pos: dict) -> bool:

        # Update deal info
        
        deal_info = selected_pos.copy()
        deal_info["direction"] = "closed"
        
        
        #... 
        #...

        print("Trade closed successfully: ", deal_info)
        
        # Save closed deal to database
        self._save_deal(deal_info, self.history_db_name)

В функции с именем _open_position.

    def _open_position(self, pos_type: str, volume: float, symbol: str, price: float, sl: float = 0.0, tp: float = 0.0, comment: str = "") -> bool:

        trade_info = self.trade_info.copy()

        #...
        #...
        #...

        self.deals_container.append(self.deal_info.copy())

        # Log to database
        self._save_deal(self.deal_info, self.history_db_name)

        return True

Ниже показана база данных SQLite, содержащая все сделки, совершенные за последние часы и дни.


Заключительные мысли

Реализуя эту начальную часть симулятора MetaTrader 5, я не могу не оценить, насколько сложен тестер стратегий MetaTrader 5. За кулисами этого инструмента происходит множество вещей, помимо простого исполнения сделок.

К этому моменту вы можете спросить себя: нужен ли этот симулятор? Ведь мы реализовали торговый симулятор, который открывает сделки так, будто это реальный счет, что, похоже, не отличается от того, что делает приложение MetaTrader 5 при использовании модуля MetaTrader5-Python.

Цель этой статьи состояла в том, чтобы понять динамику торгового симулятора: моделируя несколько простых сделок и убеждаясь, что они очень похожи на сделки, открытые на реальном счете, мы можем сказать, что приближаемся к нашей цели.

Также справедливо сказать, что этот симулятор далеко не полон и не идеален по сравнению с тестером стратегий платформы MetaTrader 5. Многое все еще отсутствует или сделано не совсем правильно, честно говоря, сложно отслеживать все детали, поэтому, если у вас есть мысли и мнения или вы хотите поучаствовать в проекте, вот ссылка на репозиторий GitHub -> https://github.com/MegaJoctan/PyMetaTester.

Что дальше?

В текущем торговом симуляторе, рассмотренном выше, мы извлекали важную информацию с рынка, такую как текущие цены ask и bid, а также другую важную информацию о выбранном символе. В следующих статьях мы обсудим различные способы извлечения тиковых данных и перебора этой информации в цикле, чтобы имитировать поведение исторического тестирования в тестере стратегий.

До встречи.


Таблица вложений

Имя файлаОписание и использование
requirements.txtСодержит все зависимости Python, используемые в этом проекте.
trade_simulator.pyСодержит класс TradeSimulator, в котором размещен весь торговый симулятор.
simulator_test.pyСкрипт-песочница для тестирования рассмотренного торгового симулятора.
toolbox_gui.pyСодержит простое GUI-приложение, похожее на MetaTrader 5, для отображения сделок и информации о балансе счета.
Trade\SymbolInfo.pyСодержит класс с именем CSymbolInfo, который предоставляет всю информацию из MetaTrader 5 о конкретном символе.
Trade\Trade.py Содержит класс с именем CTrade, который предоставляет функции для открытия позиций и ордеров в MetaTrader 5 с помощью модуля MetaTrader5-Python. 

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18971

Прикрепленные файлы |
Attachments.zip (17.05 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Anton du Plessis
Anton du Plessis | 10 авг. 2025 в 10:19
Спасибо за вашу новаторскую работу. Я с нетерпением жду возможности опробовать это.
Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов
В данной статье мы разрабатываем программу для торговли по линиям тренда, которая использует аппроксимацию методом наименьших квадратов (least squares fit) для определения линий поддержки и сопротивления, генерируя динамические сигналы на покупку и продажу при касании ценой этих линий и открывая позиции по полученным сигналам.
Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка
В этой статье рассматривается уникальный профиль каждой валютной пары через призму исторической динамики ее цены. Вдохновляясь концепцией генетической ДНК, которая задает уникальный генетический код каждого живого существа, мы применяем аналогичный подход к рынкам, рассматривая динамику цены как "ДНК" каждой валютной пары. Анализируя такие структурные характеристики, как волатильность, свинги, откаты, всплески и особенности сессий, инструмент выявляет базовый профиль, который отличает одну пару от другой. Этот подход дает более глубокое понимание поведения рынка и помогает трейдерам системно соотносить стратегии с естественными склонностями каждого инструмента.
Двумерные копулы в MQL5 (Часть 1): Реализация гауссовой копулы и t-копулы Стьюдента для моделирования зависимостей Двумерные копулы в MQL5 (Часть 1): Реализация гауссовой копулы и t-копулы Стьюдента для моделирования зависимостей
Это первая часть серии статей, посвящённых реализации двумерных копул в MQL5. В статье представлен код, реализующий гауссову копулу и t-копулу Стьюдента. Также рассматриваются основы статистических копул и связанные с ними темы. Код основан на Python-пакете ArbitrageLab от Hudson and Thames.
Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД
В статье рассказывается, как объединить SQLite (OLTP) с DuckDB (OLAP) для обработки данных статистического арбитража. Колоночный движок DuckDB, оператор ASOF JOIN и встроенные функции для работы с массивами ускоряют выполнение основных задач, таких как сопоставление котировок со сделками и RWEC, при этом зафиксировано увеличение скорости от 2 до 23 раз по сравнению с SQLite при работе с большими массивами данных. Вы получаете более простые запросы и более быструю аналитику, при этом исполнение операций по-прежнему осуществляется в SQLite.