Тестер стратегий для Python и MetaTrader 5 (Часть 1): Торговый симулятор
Содержание
- Введение
- Торговый симулятор 101
- Расчет прибыли/убытка по позиции
- Симуляция позиции
- Проверка торговых параметров
- Изменение позиций
- Мониторинг позиций
- Отложенные ордера
- Удаление отложенных ордеров
- Изменение отложенных ордеров
- Мониторинг отложенных ордеров
- Мониторинг счета
- Симуляция торговли в реальном времени на Python
- GUI-приложение для моделирования в реальном времени
- Внешнее управление и контроль позиций и ордеров
- Работа со сделками
- Заключение
Лучше делать что-то, чем ничего не делать, ожидая возможности сделать всё.
— Уинстон Черчилль.
Введение
Пакет 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) Проверка размера лота
Чтобы проверить размер лота (объем) сделки, мы проверяем три условия.
- Если заданный размер лота меньше минимально допустимого объема для символа
- Если заданный размер лота больше максимально допустимого объема для данного символа
- Если заданный размер лота кратен своему шагу (минимальному шагу изменения объема для исполнения сделки)
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 имеет некоторые сходства с открытием новой; функция выше гарантирует выполнение двух проверок перед подтверждением изменения позиции.
- Проверка того, что новый stop loss допустим согласно типу позиции: новое значение stop loss должно быть больше текущей рыночной цены позиции для позиции buy и наоборот для позиции sell.
- Проверка того, что новые значения 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; чуть позже мы также обсудим мониторинг отложенных ордеров.
Отложенные ордера
В отличие от рыночных ордеров (позиций), предназначенных для немедленного исполнения по рынку, отложенные ордера содержат приказ выполнить торговую операцию при наличии определенного условия. Отложенные ордера также могут содержать ограничение по времени действия — дату истечения ордера.
К отложенным ордерам относятся.
Пока мы реализуем первые четыре типа отложенных ордеров из списка выше в классе торгового симулятора, просто чтобы начать.
Начнем с базовой функции для размещения отложенных ордеров.
Проверки:
(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): Проверка, что все отложенные ордера не слишком близки к рынку
- Проверка, что цена открытия отложенного ордера, связанного с buy, не слишком близка к цене bid.
- Проверка, что цена открытия отложенного ордера, связанного с sell, не слишком близка к цене ask.
# 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)
В функциях выше мы добавляем условия, чтобы каждый ордер размещался хотя бы в правильном месте, т. е.
- Ордер buy stop размещается выше текущей рыночной цены (цены ask)
- Ордер buy limit размещается ниже текущей рыночной цены (цены bid)
- Ордер sell stop размещается ниже текущей рыночной цены (цены bid)
- Ордер 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): Проверка, что новая цена открытия позиции находится в правильном месте в соответствии с типом ордера.
Во всех отложенных ордерах новая цена открытия должна быть размещена:
- Выше текущей рыночной цены (цены ask) для ордера buy stop
- Ниже текущей рыночной цены (цены bid) для ордера buy limit
- Ниже текущей рыночной цены (цены bid) для ордера sell stop
- Выше текущей рыночной цены (цены 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 сделка представляет фактическое исполнение торговой операции — это результат ордера. Каждая сделка основана на конкретном ордере, но один ордер может создавать несколько сделок (например, если ордер исполняется частями).
Сделки создаются, когда.
- Позиция открывается,
- Позиция частично или полностью закрывается,
- Или ордер (например 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация торговых стратегий в MQL5 (Часть 25): Советник для торговли по линиям тренда с аппроксимацией методом наименьших квадратов и динамической генерацией сигналов
Разработка инструментария для анализа Price Action (Часть 40): ДНК-профиль рынка
Двумерные копулы в MQL5 (Часть 1): Реализация гауссовой копулы и t-копулы Стьюдента для моделирования зависимостей
Статистический арбитраж на основе коинтегрированных акций (заключительная часть): Анализ данных с помощью специализированной БД
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
