Тестер стратегий для Python и MetaTrader 5 (Часть 05): Тестер стратегий для нескольких символов и таймфреймов
Содержание
- Введение
- Обработка данных нескольких таймфреймов
- Проблема много-таймфреймового подхода
- Извлечение данных напрямую из MetaTrader 5
- Многопоточная обработка инструментов
- Дальнейшее решение проблем производительности
- Полнофункциональный мультивалютный торговый робот
- Заключение
Введение
В предыдущей статье мы смогли использовать реальные тики, сгенерированные тики и бары, полученные из терминала MetaTrader 5, в нашем пользовательском тестере стратегий. Несмотря на то что мы получили первый успешный запуск тестирования стратегии, нам ещё предстоит полноценно обрабатывать данные по разным инструментам и таймфреймам — то, с чем тестер стратегий MetaTrader 5 справляется очень хорошо.
Предположим, у вас есть мультивалютный и мультитаймфреймовый торговый робот:
Мультивалютный советник по таймфреймам EA.mq5
string symbols[] = {"EURUSD", "GBPUSD", "USDCAD"}; ENUM_TIMEFRAMES timeframes[] = {PERIOD_M15, PERIOD_H1, PERIOD_H4, PERIOD_D1}; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- for (uint s=0; s<symbols.Size(); s++) for (uint t=0; t<timeframes.Size(); t++) { string symbol = symbols[s]; ENUM_TIMEFRAMES tf = timeframes[t]; double open = iOpen(symbol, tf, 0); printf("symbol: %s tf: %s | Current candle's opening = %.5f",symbol, EnumToString(tf), open); } }
Вывод в тестере стратегий:
CS 0 12:27:55.690 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:00 symbol: USDCAD tf: PERIOD_D1 | Current candle's opening = 1.39191 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: EURUSD tf: PERIOD_M15 | Current candle's opening = 1.17328 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: EURUSD tf: PERIOD_H1 | Current candle's opening = 1.17328 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: EURUSD tf: PERIOD_H4 | Current candle's opening = 1.17328 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: EURUSD tf: PERIOD_D1 | Current candle's opening = 1.17328 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: GBPUSD tf: PERIOD_M15 | Current candle's opening = 1.34442 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: GBPUSD tf: PERIOD_H1 | Current candle's opening = 1.34442 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: GBPUSD tf: PERIOD_H4 | Current candle's opening = 1.34442 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: GBPUSD tf: PERIOD_D1 | Current candle's opening = 1.34442 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: USDCAD tf: PERIOD_M15 | Current candle's opening = 1.39191 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: USDCAD tf: PERIOD_H1 | Current candle's opening = 1.39191 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: USDCAD tf: PERIOD_H4 | Current candle's opening = 1.39191 CS 0 12:28:06.274 Multicurrency timeframe EA (EURUSD,H1) 2025.10.01 00:00:05 symbol: USDCAD tf: PERIOD_D1 | Current candle's opening = 1.39191
Хотя тестер стратегий был запущен на часовом таймфрейме (H1), он всё равно имел доступ к данным по разным инструментам и таймфреймам — то есть к большему объёму данных, чем пользователь запросил изначально.
Именно эта интересная способность терминала делает возможными мультивалютные торговые системы в MetaTrader 5.
В этой статье мы реализуем похожий способ обработки баров по разным инструментам и таймфреймам в нашем пользовательском симуляторе MetaTrader 5-Python.
Обработка данных нескольких таймфреймов
Согласно тому, как мы спроектировали пользовательский класс тестера стратегий, во время инициализации класса мы собираем всю историю, необходимую для всего процесса тестирования стратегии, и сохраняем её в соответствующие parquet-файлы в локальной папке рядом со скриптом (по умолчанию — в папке с именем History. Она расположена в том же пути, где находится основной скрипт).
В отличие от предыдущей версии тестера стратегий, где подобный код был разбросан по разным местам, теперь вся эта логика инкапсулирована в класс HistoryManager.
Для начала установите все зависимости из файла requirements.txt, приложенного в конце этой статьи, в своё виртуальное окружение Python.
pip install -r requirements.txt
StrategyTester5/hist/manage.py
from strategytester5 import STRING2TIMEFRAME_MAP, TIMEFRAME2STRING_MAP from strategytester5.hist import ticks, bars from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed from datetime import datetime import time import os from typing import Any, Self from typing import Optional import logging class HistoryManager: def __init__(self, mt5_instance: Any, symbols: list, start_dt: datetime, end_dt: datetime, timeframe: int, LOGGER: Optional[logging.Logger] = None, max_fetch_workers: int=None, max_cpu_workers: int=None, history_dir: str = "History" ): """Initialize a history manager for fetching and storing MT5 data. Args: mt5_instance: MT5 API/client instance used to fetch data. symbols: List of symbol strings to retrieve history for. start_dt: Inclusive start datetime for the history window. end_dt: Inclusive end datetime for the history window. timeframe: MT5 timeframe constant (e.g., mt5.TIMEFRAME_M1). LOGGER: A Logger instance, max_fetch_workers: Max concurrent fetch workers; defaults based on symbol count. max_cpu_workers: Max CPU workers for processing; defaults to (CPU count - 1). history_dir: Directory name for persisted history data. """ self.mt5_instance = mt5_instance self.symbols = symbols self.start_dt = start_dt self.end_dt = end_dt self.max_fetch_workers = max_fetch_workers self.max_cpu_workers = max_cpu_workers self.LOGGER = LOGGER self.history_dir = history_dir self.timeframe = timeframe if max_fetch_workers is None: self.max_fetch_workers = min(32, max(4, len(self.symbols))) if max_cpu_workers is None: self.max_cpu_workers = max(1, os.cpu_count() - 1) def __critical_log(self, msg: str): """Log a critical message via LOGGER or print as fallback.""" if self.LOGGER is not None: self.LOGGER.critical(msg) else: print(msg) def __info_log(self, msg: str): """Log an info message via LOGGER or print as fallback.""" if self.LOGGER is not None: self.LOGGER.info(msg) else: print(msg) def _fetch_bars_worker(self, symbol: str, timeframe: int, return_df: bool=False) -> dict: """Fetch historical bars for a symbol and return summary info when requested. Args: symbol: Instrument symbol to fetch. timeframe: MT5 timeframe constant to query. return_df: If True, return a dict with bars and metadata; else {}. """ bars_obtained = bars.fetch_historical_bars( which_mt5=self.mt5_instance, symbol=symbol, timeframe=timeframe, start_datetime=self.start_dt, end_datetime=self.end_dt, return_df=return_df, LOGGER=self.LOGGER, hist_dir=self.history_dir ) bars_info = { "symbol": symbol, "bars": bars_obtained, "size": bars_obtained.height, "counter": 0 } return bars_info if return_df else {} def _fetch_ticks_worker(self, symbol: str, return_df: bool=False) -> dict: """Fetch real ticks for a symbol and return summary info when requested. Args: symbol: Instrument symbol to fetch. return_df: If True, return a dict with ticks and metadata; else {}. """ ticks_obtained = ticks.fetch_historical_ticks( which_mt5=self.mt5_instance, start_datetime=self.start_dt, end_datetime=self.end_dt, symbol=symbol, return_df=True, LOGGER=self.LOGGER, hist_dir=self.history_dir ) ticks_info = { "symbol": symbol, "ticks": ticks_obtained, "size": ticks_obtained.height, "counter": 0 } return ticks_info if return_df else {} def _gen_ticks_worker(self, symbol: str, symbol_points: float, return_df: bool=False) -> dict: """Generate synthetic ticks from M1 bars for a symbol and saves data. Args: symbol: Instrument symbol to generate ticks for. return_df: If True, return a dict with ticks and metadata; else {}. """ one_minute_bars = bars.fetch_historical_bars( which_mt5=self.mt5_instance, symbol=symbol, timeframe=STRING2TIMEFRAME_MAP["M1"], # <- use your map key directly start_datetime=self.start_dt, end_datetime=self.end_dt, LOGGER=self.LOGGER, hist_dir=self.history_dir, return_df=return_df ) if one_minute_bars is None: return {} ticks_df = ticks.TicksGen.generate_ticks_from_bars( bars=one_minute_bars, symbol=symbol, symbol_point=symbol_points, LOGGER=self.LOGGER, hist_dir=self.history_dir, return_df=True ) ticks_info = { "symbol": symbol, "ticks": ticks_df, "size": ticks_df.height, "counter": 0 } return ticks_info if return_df else {} def fetch_history(self, modelling: str, symbol_info_func: any): """Fetch bars or ticks for all symbols according to the modelling mode. Args: modelling: One of "real_ticks", "every_tick", "new_bar", "1-minute-ohlc". Returns: Tuple of (all_bars_info, all_ticks_info) lists. """ all_ticks_info = [] all_bars_info = [] if modelling == "real_ticks": start_time = time.time() with ThreadPoolExecutor(max_workers=self.max_fetch_workers) as executor: futs = {executor.submit(self._fetch_ticks_worker, s, True): s for s in self.symbols} for fut in as_completed(futs): sym = futs[fut] try: res = fut.result() # <- get dict all_ticks_info.append(res) except Exception as e: self.__critical_log(f"Failed to fetch real ticks for {sym}: {e!r}") total_ticks = sum(info["size"] for info in all_ticks_info) self.__info_log(f"Total real ticks collected: {total_ticks} in {(time.time()-start_time):.2f} seconds.") elif modelling == "every_tick": start_time = time.time() with ThreadPoolExecutor(max_workers=self.max_fetch_workers) as executor: futs = {executor.submit(self._gen_ticks_worker,s, symbol_info_func(s).point, True): s for s in self.symbols} for fut in as_completed(futs): sym = futs[fut] try: res = fut.result() # <- get dict all_ticks_info.append(res) except Exception as e: self.__critical_log(f"Failed to generate ticks for {sym}: {e!r}") total_ticks = sum(info["size"] for info in all_ticks_info) self.__info_log(f"Total ticks generated: {total_ticks} in {(time.time()-start_time):.2f} seconds.") elif modelling in ("new_bar", "1-minute-ohlc"): start_time = time.time() tf = STRING2TIMEFRAME_MAP["M1"] if modelling == "1-minute-ohlc" else STRING2TIMEFRAME_MAP[self.timeframe] with ThreadPoolExecutor(max_workers=self.max_fetch_workers) as executor: futs = {executor.submit(self._fetch_bars_worker, s, tf, True): s for s in self.symbols} for fut in as_completed(futs): sym = futs[fut] try: res = fut.result() # <- get dict all_bars_info.append(res) except Exception as e: self.__critical_log(f"Failed to fetch bars for {sym}: {e!r}") total_bars = sum(info["size"] for info in all_bars_info) self.__info_log(f"Total bars collected: {total_bars} from '{TIMEFRAME2STRING_MAP[tf]}' timeframe in {(time.time()-start_time):.2f} seconds.") return all_bars_info, all_ticks_info def synchronize_timeframes(self): all_tfs = list(STRING2TIMEFRAME_MAP.values()) start = time.time() with ThreadPoolExecutor(max_workers=self.max_fetch_workers) as ex: futs = {ex.submit(self._fetch_bars_worker, sym, tf, False): (sym, tf) for sym in self.symbols for tf in all_tfs} for fut in as_completed(futs): sym, tf = futs[fut] try: fut.result() except Exception as e: self.__critical_log(f"sync failed {sym} {TIMEFRAME2STRING_MAP.get(tf, tf)}: {e!r}") self.__info_log(f"Timeframes synchronization complete! {(time.time() - start):.2f}s elapsed.")
В классе есть 3 отдельные функции, имена которых заканчиваются на _worker;
- _fetch_bars_worker: вызывает функцию fetch_historical_bars из bars.py которая получает данные баров из терминала MetaTrader 5 и сохраняет их в указанном месте, в подпапке с именем Bars
- _fetch_ticks_worker: вызывает функцию fetch_historical_ticks, которая получает тики за заданный диапазон дат из терминала MetaTrader 5; результирующий DataFrame Polars сохраняется в нужном месте, в подпапке с именем Ticks
- _gen_ticks_worker: эта функция вызывает метод generate_ticks_from_bars из класса TicksGen, этот класс содержит методы, отвечающие за генерацию синтетических тиков на основе баров минутного таймфрейма.
Затем все эти функции вызываются при определённых условиях внутри функции с именем fetch_history. В отличие от предыдущей версии, на этот раз мы собираем и бары, и тики в многопоточной среде.
Это ускоряет работу: время загрузки истории сократилось более чем на 50% по сравнению с предыдущими версиями пользовательского тестера стратегий.
Функцию получения истории мы вызываем внутри конструктора StrategyTester.
class StrategyTester: def __init__(self, tester_config: dict, mt5_instance: MetaTrader5, logs_dir: Optional[str]="Logs", reports_dir: Optional[str]="Reports", history_dir: Optional[str]="History"): """MetaTrader 5-Like Strategy tester for the MetaTrader5-Python module. Args: tester_config: Dictionary of tester configuration values. mt5_instance: MetaTrader5 API/client instance used for obtaining crucial information from the broker as an attempt to mimic the terminal. logs_dir: Directory for log files. reports_dir: Directory for HTML reports and assets. history_dir: Directory for historical data storage. Raises: RuntimeError: If required MT5 account info cannot be obtained. """ #... other variables # --------------- initialize ticks or bars data ---------------------------- self.logger.info("StrategyTester Initializing") self.logger.info(f"StrategyTester configs: {self.tester_config}") hist_manager = HistoryManager(mt5_instance=self.mt5_instance, symbols=self.tester_config["symbols"], start_dt=self.tester_config["start_date"], end_dt=self.tester_config["end_date"], timeframe=self.tester_config["timeframe"], history_dir=self.history_dir, LOGGER=self.logger ) for symbol in self.tester_config["symbols"]: self.symbol_info(symbol) self.TESTER_ALL_BARS_INFO, self.TESTER_ALL_TICKS_INFO = hist_manager.fetch_history( self.tester_config["modelling"], symbol_info_func=self.symbol_info, )
Сразу после получения истории мы синхронизируем бары со всех таймфреймов (только если пользователь запускает программу в режиме MetaTrader 5).
if not self.IS_TESTER:
hist_manager.synchronize_timeframes() Цель синхронизации всех таймфреймов из MetaTrader 5 для всех указанных символов (инструментов) состоит в том, чтобы бары всех таймфреймов были собраны и сохранены в локальной папке рядом со скриптом для последующего доступа, например когда пользователь вызывает методы «копирования котировок» внутри симулятора.
Все данные баров должны быть доступны постоянно, как и в терминале MetaTrader 5.
Конфигурация для нескольких символов:
examples/tester.json
{
"tester": {
"bot_name": "MY EA",
"symbols": ["EURUSD", "USDCAD", "USDJPY"],
"timeframe": "H1",
"start_date": "01.10.2025 00:00",
"end_date": "31.12.2025 00:00",
"modelling" : "new_bar",
"deposit": 1000,
"leverage": "1:100"
}
} Вывод.
D:\StrategyTester5\.env\Scripts\python.exe D:\StrategyTester5\examples\example_bot.py 2026-01-24 21:53:10,039 | INFO | MY EA.tester | [tester.py:88 - __init__() ] => StrategyTester Initializing 2026-01-24 21:53:10,040 | INFO | MY EA.tester | [tester.py:89 - __init__() ] => StrategyTester configs: {'bot_name': 'MY EA', 'symbols': ['EURUSD', 'USDCAD', 'USDJPY'], 'timeframe': 'H1', 'modelling': 'new_bar', 'start_date': datetime.datetime(2025, 10, 1, 0, 0), 'end_date': datetime.datetime(2025, 12, 31, 0, 0), 'deposit': 1000.0, 'leverage': 100} 2026-01-24 21:53:10,041 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (H1): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,044 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H1): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,045 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (H1): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,084 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,084 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (H1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,084 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (H1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,088 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (H1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,088 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,089 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (H1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,093 | INFO | MY EA.tester | [manager.py:63 - __info_log() ] => Total bars collected: 4611 from 'H1' timeframe in 0.05 seconds. 2026-01-24 21:53:10,094 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M1): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,111 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M2): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,127 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M3): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,128 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M4): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,168 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M3): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,169 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M2): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,170 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M4): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,170 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,266 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M15): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,266 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M10): 2025-12-01 -> 2025-12-31 2026-01-24 21:58:39,999 | INFO | MY EA.tester | [manager.py:247 - synchronize_timeframes() ] => Synchronizing timeframes... 2026-01-24 21:53:10,267 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed EURUSD M5: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,267 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (M12): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,327 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed EURUSD M15: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,327 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (H1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,344 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (H3): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,346 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed EURUSD M20: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,347 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (H2): 2025-11-01 -> 2025-11-30ribute 'height'") 2026-01-24 21:53:10,476 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M2): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,476 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M3): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,506 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for EURUSD (MN1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,507 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed EURUSD D1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,535 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed EURUSD W1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,536 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M4): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,537 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed EURUSD MN1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,546 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M3): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,546 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M2): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,558 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,882 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD M12: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,883 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M20): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,883 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (M30): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,892 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H2): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,892 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,892 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD M15: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,905 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H3): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,908 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H4): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:10,908 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD M30: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,920 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:10,920 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H2): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:10,938 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD M20: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:10,938 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (H4): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,018 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD H8: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,029 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M1): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:11,029 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M2): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:11,030 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD D1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,031 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (W1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,051 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (MN1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,066 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD H12: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,071 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M3): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:11,071 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD W1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,093 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M2): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,093 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDCAD (MN1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,095 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,101 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M4): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:11,102 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDCAD MN1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,102 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M3): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,113 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M2): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,119 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M4): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,124 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M1): 2025-12-01 -> 2025-12-31ight'") 2026-01-24 21:53:11,307 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (M30): 2025-10-01 -> 2025-10-31ribute 'height'") 2026-01-24 21:53:11,344 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (H1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,345 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (H2): 2025-10-01 -> 2025-10-31 2026-01-24 21:53:11,468 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDJPY H4: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,483 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (W1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,484 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (H12): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,485 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDJPY H8: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,485 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (MN1): 2025-11-01 -> 2025-11-30 2026-01-24 21:53:11,486 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (D1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,488 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (W1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,489 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDJPY H12: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,492 | INFO | MY EA.tester | [bars.py:56 - fetch_historical_bars() ] => Processing bars for USDJPY (MN1): 2025-12-01 -> 2025-12-31 2026-01-24 21:53:11,492 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDJPY D1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,493 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDJPY W1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,494 | CRITICAL | MY EA.tester | [manager.py:56 - __critical_log() ] => sync failed USDJPY MN1: AttributeError("'NoneType' object has no attribute 'height'") 2026-01-24 21:53:11,494 | INFO | MY EA.tester | [manager.py:63 - __info_log() ] => Timeframes synchronization complete! 1.40s elapsed.
Теперь попробуем сделать в Python то же, что раньше делали в MQL5: получить текущую цену открытия с нескольких таймфреймов (15 минут, один час, 4 часа, дневной).
examples/example_bot.py
import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import MetaTrader5 as mt5 from strategytester5.tester import StrategyTester, TIMEFRAME2STRING_MAP import json import os from datetime import datetime import pandas as pd if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit() # Get path to the folder where this script lives BASE_DIR = os.path.dirname(os.path.abspath(__file__)) try: with open(os.path.join(BASE_DIR, "tester.json"), 'r', encoding='utf-8') as file: # reading a JSON file # Deserialize the file data into a Python object configs_json = json.load(file) except Exception as e: raise RuntimeError(e) tester_configs = configs_json["tester"] tester = StrategyTester(tester_config=tester_configs, mt5_instance=mt5) # very important # ------------- global variables ---------------- symbols = tester_configs["symbols"] timeframes = [mt5.TIMEFRAME_M15, mt5.TIMEFRAME_H1, mt5.TIMEFRAME_H4, mt5.TIMEFRAME_D1] # --------------------------------------------------------- def on_tick(): for symbol in symbols: for tf in timeframes: rates = tester.copy_rates_from_pos(symbol=symbol, timeframe=tf, start_pos=0, count=5) if rates is None: continue if len(rates) ==0: continue open_price = rates[-1]["open"] # current opening price, the latest one in the array time = datetime.fromtimestamp(rates[-1]["time"]) print(f"{time} : symbol: {symbol} tf: {TIMEFRAME2STRING_MAP[tf]} | Current candle's opening = {open_price:.5f}"); tester.OnTick(ontick_func=on_tick) # very important!
В отличие от языка программирования MQL5, Python обычно прерывает выполнение всей программы при возникновении исключения. Чтобы этого избежать, нам нужно реализовать несколько операторов if, которые будут пропускать текущую итерацию, если пользовательский тестер не вернул данные по барам.
Обратите внимание, что мы получили информацию (цены открытия) по всем инструментам, выбранным в конфигурационном JSON-файле.
Именно так работает наш пользовательский тестер стратегий — всё предсказуемо. Все инструменты, используемые в торговом роботе, должны быть заранее определены.
Вывод.
StrategyTester Progress: 0%| | 14/4611 [00:00<04:23, 17.44bar/s] 2025-10-06 03:00:00 : symbol: EURUSD tf: D1 | Current candle's opening = 1.17180 2025-10-01 08:15:00 : symbol: USDCAD tf: M15 | Current candle's opening = 1.39270 2025-10-01 12:00:00 : symbol: USDCAD tf: H1 | Current candle's opening = 1.39144 2025-10-02 03:00:00 : symbol: USDCAD tf: H4 | Current candle's opening = 1.39342 2025-10-06 03:00:00 : symbol: USDCAD tf: D1 | Current candle's opening = 1.39406 2025-10-01 08:15:00 : symbol: USDJPY tf: M15 | Current candle's opening = 147.95500 2025-10-01 12:00:00 : symbol: USDJPY tf: H1 | Current candle's opening = 147.51700 2025-10-02 03:00:00 : symbol: USDJPY tf: H4 | Current candle's opening = 147.03700 2025-10-06 03:00:00 : symbol: USDJPY tf: D1 | Current candle's opening = 149.44800 2025-10-01 08:15:00 : symbol: EURUSD tf: M15 | Current candle's opening = 1.17331 2025-10-01 12:00:00 : symbol: EURUSD tf: H1 | Current candle's opening = 1.17525 2025-10-02 03:00:00 : symbol: EURUSD tf: H4 | Current candle's opening = 1.17248 2025-10-06 03:00:00 : symbol: EURUSD tf: D1 | Current candle's opening = 1.17180 2025-10-01 08:15:00 : symbol: USDCAD tf: M15 | Current candle's opening = 1.39270 2025-10-01 12:00:00 : symbol: USDCAD tf: H1 | Current candle's opening = 1.39144 2025-10-02 03:00:00 : symbol: USDCAD tf: H4 | Current candle's opening = 1.39342 2025-10-06 03:00:00 : symbol: USDCAD tf: D1 | Current candle's opening = 1.39406
Проблема много-таймфреймового подхода
Когда приведённый выше код в скрипте был запущен в режиме моделирования real_ticks , тестер стратегий стал чрезвычайно медленным.

При скорости около 13 тиков в секунду один тест мог бы выполняться неделями.
Это связано с тем, как мы организовали управление данными в проекте. Когда пользователь запрашивает информацию о барах или тиках, мы читаем её напрямую из parquet-файлов, а, как известно, операции ввода-вывода относятся к самым медленным операциям в большинстве языков программирования.
Одним из оптимальных решений могло бы быть хранение всей этой информации в памяти и чтение её оттуда, но это вызывает другую проблему — огромный объём данных. Не всегда надёжно хранить всю информацию о тиках и барах в памяти одновременно.
На данный момент мы могли бы решить эту проблему двумя способами :
- Извлекать все данные (тики и бары) напрямую из терминала MetaTrader 5
- Организовать параллельную обработку инструментов/символов
Извлечение данных напрямую из терминала MetaTrader 5
Во второй статье этой серии, мы представили синтаксис, похожий на тот, который предоставляет MetaTrader 5 API, с возможностью выбрать, полагаться ли на терминал MetaTrader 5 как источник информации или читать данные из ближайшей папки (пути истории симулятора).
Когда итоговый скрипт вызывается с аргументом --mt5, симулятор использует MetaTrader 5 API для получения информации напрямую.
(.env) D:\StrategyTester5\examples\multicurrency trading bot>python bot.py --mt5 Когда скрипт, обсуждавшийся в предыдущем разделе, был запущен в режиме MetaTrader 5, производительность (скорость) немного улучшилась.
(.env) D:\StrategyTester5\examples\multicurrency trading bot>python single_thread.py --mt5 2026-01-28 18:30:53,816 | INFO | .mt5 | [tester.py:81 - __init__() ] => MT5 mode 2026-01-28 18:30:53,817 | INFO | .mt5 | [tester.py:88 - __init__() ] => MT5 mode 2026-01-28 18:30:53,817 | INFO | .mt5 | [tester.py:92 - __init__() ] => StrategyTester Initializing 2026-01-28 18:30:53,817 | INFO | .mt5 | [tester.py:93 - __init__() ] => StrategyTester configs: {'bot_name': 'MY EA', 'symbols': ['EURUSD', 'GBPUSD', 'USDCAD'], 'timeframe': 'H1', 'modelling': 'real_ticks', 'start_date': datetime.datetime(2025, 10, 1, 0, 0), 'end_date': datetime.datetime(2025, 12, 31, 0, 0), 'deposit': 1000.0, 'leverage': 100} 2026-01-28 18:30:53,819 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for EURUSD: 2025-10-01 -> 2025-10-31 2026-01-28 18:30:53,959 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for GBPUSD: 2025-10-01 -> 2025-10-31 2026-01-28 18:30:54,137 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for USDCAD: 2025-10-01 -> 2025-10-31 2026-01-28 18:30:54,676 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for USDCAD: 2025-11-01 -> 2025-11-30 2026-01-28 18:30:54,967 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for EURUSD: 2025-11-01 -> 2025-11-30 2026-01-28 18:30:54,967 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for GBPUSD: 2025-11-01 -> 2025-11-30 2026-01-28 18:30:55,676 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for USDCAD: 2025-12-01 -> 2025-12-31 2026-01-28 18:30:55,963 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for EURUSD: 2025-12-01 -> 2025-12-31 2026-01-28 18:30:55,963 | INFO | .mt5 | [ticks.py:53 - fetch_historical_ticks() ] => Processing ticks for GBPUSD: 2025-12-01 -> 2025-12-31 2026-01-28 18:30:56,972 | INFO | .mt5 | [manager.py:63 - __info_log() ] => Total real ticks collected: 11893287 in 3.15 seconds. 2026-01-28 18:30:56,972 | INFO | .mt5 |` [tester.py:115 - __init__() ] => Initialized StrategyTester Progress: 1%|▌ | 88795/11893287 [03:44<7:06:56, 560.81tick/s]
С 13 тиков в секунду почти до 600 тиков/с. Это улучшение, но всё ещё недостаточное, учитывая большое количество тиков, которое может быть доступно даже за короткий период времени.
Например, за один год (с 1 января 2025 года по 31 декабря 2025 года) было доступно 11 миллионов тиков. При текущем темпе один тест всё равно выполнялся бы очень долго.
Многопоточная обработка инструментов
В мультивалютной торговой системе все ресурсы счёта, такие как баланс, средства на счёте (equity), свободная маржа и т. д., являются общими для всех инструментов, но торговые операции изолированы.
Учитывая это, мы можем изменить то, как функция OnTick в классе StrategyTester обрабатывает основную пользовательскую функцию OnTick и как она сама себя ведёт.
Изначально мы запрограммировали эту функцию так, чтобы она последовательно проходила по всем доступным тикам всех инструментов. Однако у такого подхода есть серьёзный недостаток.
Она вызывает пользовательскую функцию OnTick на каждой тиковой итерации, даже для несвязанных тиков других символов. Эти лишние вызовы создают ненужные вычислительные затраты и замедляют всю программу.

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

Для параллельной обработки нам нужна функция для каждого рабочего потока (в данном случае — для каждого символа).
tester.py
def __ontick_symbol(self, symbol: str, modelling: str, ontick_func: any): info = None is_tick_mode = False if modelling in ("new_bar", "1-minute-ohlc"): info = next( (x for x in self.TESTER_ALL_BARS_INFO if x["symbol"] == symbol), None, ) if modelling in ("real_ticks", "every_tick"): info = next( (x for x in self.TESTER_ALL_TICKS_INFO if x["symbol"] == symbol), None, ) is_tick_mode = True if info is None: return ticks = info["ticks"] if is_tick_mode else info["bars"] size = info["size"] self.logger.info(f"{symbol} total number of ticks: {size}") local_idx = 0 with tqdm(total=size, desc=f"StrategyTester Progress on {symbol}", unit="tick" if is_tick_mode else "bar") as pbar: while local_idx < size: # tick = None if is_tick_mode: tick = ticks.row(local_idx) else: tick = self._bar_to_tick(symbol=symbol, bar=ticks.row(local_idx)) # a bar=tick is not actually a tick, rather a bar local_idx += 1 if tick is None: pbar.update(1) continue # Critical section: only one thread at a time with self._engine_lock: self.TickUpdate(symbol=symbol, tick=tick) ontick_func(symbol) # each symbol processed in a separate thread self.__account_monitoring() # monitor only when positions of such symbol exists if len(self.positions_get(symbol=symbol)) > 0: self.__positions_monitoring() # monitor only when orders of such symbol exists if len(self.orders_get(symbol=symbol)) > 0: self.__pending_orders_monitoring() if isinstance(tick, dict): time = tick["time_msc"] elif isinstance(tick, tuple): tick = make_tick_from_tuple(tick) time = tick.time_msc else: self.logger.error("Unknown tick type") continue if self.positions_total() > 0: self.__curves_update(index=self.CURVES_IDX, time=time) self.CURVES_IDX+=1 self.TESTER_IDX += 1 pbar.update(1)
Вместо перезаписи функции, которую мы создали ранее, мы вводим новую функцию OnTick для параллельного тестирования стратегии.
tester.py
def ParallelOnTick(self, ontick_func): """Calls the assigned function upon the receival of new tick(s) Args: ontick_func (_type_): A function to be called on every tick """ self.__validate_ontick_signature(ontick_func) symbols = self.tester_config["symbols"] modelling = self.tester_config["modelling"] max_workers = len(symbols) size = 0 if modelling in ("new_bar", "1-minute-ohlc"): size = sum(bars_info["size"] for bars_info in self.TESTER_ALL_BARS_INFO) if modelling in ("real_ticks", "every_tick"): size = sum(ticks_info["size"] if ticks_info else 0 for ticks_info in self.TESTER_ALL_TICKS_INFO) self.__TesterInit(size=size) with ThreadPoolExecutor(max_workers=max_workers) as executor: futs = {executor.submit(self.__ontick_symbol, s, modelling, ontick_func): s for s in symbols} # wait + raise exceptions for fut in as_completed(futs): fut.result() self.__TesterDeinit()
При таком подходе пользовательская функция OnTick должна принимать аргумент symbol перед передачей её в указанную выше функцию.
parallel.py
def on_tick_multicurrency(symbol: str): for tf in timeframes: rates = tester.copy_rates_from_pos(symbol=symbol, timeframe=tf, start_pos=0, count=5) if rates is None: continue if len(rates) == 0: continue tester.ParallelOnTick(ontick_func=on_tick_multicurrency) # very important!
Попробуем снова запустить скрипт и посмотрим на его производительность.
StrategyTester Progress on EURUSD: 0%|▏ | 10118/3608681 [14:18<70:46:49, 14.12tick/s] StrategyTester Progress on USDCAD: 0%|▏ | 10391/3174201 [14:18<60:26:21, 14.54tick/s] StrategyTester Progress on GBPUSD: 0%| | 9993/5110405 [14:17<128:16:10, 11.05tick/s]
Хотя скорость обработки тиков для каждого инструмента остаётся прежней поскольку мы пока не устранили реальную причину медленного выполнения тестирования стратегии, параллельная обработка тиков сокращает общее время обработки всех доступных тиков, так как тики по всем инструментам обрабатываются одновременно.
В этом случае тестирование трёх инструментов гарантированно завершится примерно за то же время, которое потребовалось бы для тестирования одного инструмента.
В предыдущем разделе мы увидели, что использование MetaTrader 5 как прямого источника данных обеспечивает лучшую производительность по сравнению с нашей пользовательской историей. Теперь попробуем этот подход в режиме MetaTrader 5.
(.env) D:\StrategyTester5\examples\multicurrency trading bot>python parallel.py --mt5 Вывод.
2026-02-03 14:48:12,013 | INFO | .mt5 | [tester.py:2059 - __ontick_symbol() ] => EURUSD total number of ticks: 3608681 2026-02-03 14:48:12,013 | INFO | .mt5 | [tester.py:2059 - __ontick_symbol() ] => GBPUSD total number of ticks: 5110405 2026-02-03 14:48:12,013 | INFO | .mt5 | [tester.py:2059 - __ontick_symbol() ] => USDCAD total number of ticks: 3174201 StrategyTester Progress on EURUSD: 2%|█▍ | 82545/3608681 [03:20<2:36:18, 375.97tick/s] StrategyTester Progress on USDCAD: 3%|█▌ | 82996/3174201 [03:20<1:58:49, 433.55tick/s] StrategyTester Progress on GBPUSD: 1%|▉ | 75724/5110405 [03:20<3:22:24, 414.57tick/s]
Дальнейшее решение проблем производительности
Есть ещё ряд шагов, которые можно предпринять, и привычек, которых стоит избегать, чтобы улучшить общую производительность торговых роботов при тестировании с помощью модуля StrategyTester5, включая:
A: избегайте лишних вызовов функций для методов, получающих историю
Методы класса StrategyTester, такие как:
- copy_rates_range
- copy_rates_from_pos
- copy_rates_from
- copy_ticks_from
- copy_ticks_range
являются самыми затратными по времени и вычислительным ресурсам методами, поскольку они читают информацию из parquet-файлов и создают операции ввода-вывода.
Если ваш торговый робот использует какие-либо из этих методов для расчёта индикаторов или других задач, следует избегать слишком частых вызовов.
В предыдущем примере мы вызывали метод copy_rates_from_pos на каждом тике от 3 разных инструментов и, что ещё хуже, четыре раза на одном тике — итеративно по 4 разным таймфреймам.
Хотя мы делали похожее и в советнике на MQL5, где терминал работал очень быстро, будто эта вычислительно дорогая операция ничего не стоила, наш класс пока далёк от такого уровня, не говоря уже о том, что Python сам по себе медленнее.
Лучший способ избежать ненужных вызовов функций — использовать обработчик события нового бара.
Мы реализуем простой вариант, который опирается на текущее время и таймфрейм.
def is_newbar(current_time: datetime, tf: int) -> bool: """A function to help in detecting the opening of a bar""" tf_seconds = PeriodSeconds(tf) curr_ts = int(current_time.timestamp()) return curr_ts % tf_seconds == 0
Функция PeriodSeconds похожа на PeriodSeconds из языка программирования MQL5; её можно импортировать напрямую из модуля.
from strategytester5 import PeriodSeconds, TIMEFRAME2STRING_MAP
Теперь вместо получения котировок (данных баров) на каждом тике, что является плохой идеей, поскольку информация баров меняется только при появлении нового бара, мы получаем сведения о барах только при обнаружении нового бара.
parallel.py
def on_tick_multicurrency(symbol: str): for tf in timeframes: rates = None if is_newbar(tester.current_time, tf): rates = tester.copy_rates_from_pos(symbol=symbol, timeframe=tf, start_pos=0, count=5) print(f"new bar at: {tester.current_time} on symbol: {symbol} tf: {TIMEFRAME2STRING_MAP[tf]}") if rates is None: continue if len(rates) == 0: continue return
Вывод.
StrategyTester Progress on EURUSD: 2%|█▍ | 83165/3608681 [00:11<07:08, 8227.49tick/s] StrategyTester Progress on GBPUSD: 1%|▊ | 68224/5110405 [00:11<10:06, 8313.69tick/s] StrategyTester Progress on USDCAD: 3%|█▋ | 83129/3174201 [00:11<09:02, 5702.92tick/s]
Скорость StrategyTester заметно улучшилась.
В режиме MetaTrader 5 результат оказался ещё лучше.
StrategyTester Progress on USDCAD: 6%|███▍ | 177558/3174201 [00:12<03:21, 14880.46tick/s] StrategyTester Progress on EURUSD: 5%|███▏ | 186969/3608681 [00:12<05:20, 10690.13tick/s] StrategyTester Progress on GBPUSD: 3%|██▏ | 178474/5110405 [00:12<03:50, 21413.38tick/s]
B: выбор подходящего режима моделирования для программы
Тестирование стратегии на реальных тиках даёт самый точный и надёжный торговый результат, однако оно вычислительно затратно и очень медленно. Между скоростью и точностью существует компромисс.
Нужно разумно выбирать подходящее моделирование в зависимости от потребностей программы. Если стратегия не зависит от операций на каждом тике, режим нового бара — наименее точный, но самый быстрый — может подойти для такой программы.
Таймфрейм 1-minute OHLC подходит для большинства случаев, поскольку обеспечивает хороший баланс между точностью и скоростью.
При небольшом количестве тиков для перебора (в зависимости от минутных баров) тест за целый год занял 17 секунд.
StrategyTester Progress on GBPUSD: 100%|██████████████████████████████████████████████████████████████████| 92056/92056 [00:17<00:00, 5342.72bar/s] StrategyTester Progress on EURUSD: 100%|██████████████████████████████████████████████████████████████████| 92066/92066 [00:17<00:00, 5237.11bar/s] StrategyTester Progress on USDCAD: 100%|██████████████████████████████████████████████████████████████████| 92062/92062 [00:17<00:00, 5116.49bar/s]
Полнофункциональный мультивалютный торговый робот
Теперь, когда мы понимаем, как мультивалютность работает в нашем симуляторе и какие меры предосторожности нужно соблюдать при её тестировании, создадим простой, но полнофункциональный торговый робот на Python, который торгует несколькими валютами.
Шаг 01: нам нужен конфигурационный JSON-файл
tester.json
{
"tester": {
"bot_name": "multi-curency-EA",
"symbols": ["EURUSD", "GBPUSD", "USDCAD"],
"timeframe": "H1",
"start_date": "01.01.2025 00:00",
"end_date": "31.12.2025 00:00",
"modelling" : "new_bar",
"deposit": 1000,
"leverage": "1:100"
}
} Повторим: все инструменты, необходимые программе, должны быть заранее определены в конфигурационном JSON-файле.
Затем этот файл загружается первым делом в скрипте после импортов.
multicurrency trading bot/parallel.py
# Get path to the folder where this script lives BASE_DIR = os.path.dirname(os.path.abspath(__file__)) try: with open(os.path.join(BASE_DIR, "tester.json"), 'r', encoding='utf-8') as file: # reading a JSON file # Deserialize the file data into a Python object configs_json = json.load(file) except Exception as e: raise RuntimeError(e) tester_configs = configs_json["tester"]
Шаг 02: мы инициализируем экземпляр MetaTrader 5 и передаём его объект классу StrategyTester
if not mt5.initialize(): raise RuntimeError(f"Failed to initialize MT5, Error = {mt5.last_error()}") tester = StrategyTester(mt5_instance=mt5, tester_config=tester_configs, logging_level=logging.CRITICAL, POLARS_COLLECT_ENGINE="streaming")
Примечание:
Не следует импортировать модуль MetaTrader 5 напрямую; всегда импортируйте его из модуля strategytester5.
from strategytester5.tester import StrategyTester, MetaTrader5 as mt5
Если вы не запускаете программы в режиме MetaTrader 5 (с аргументом --mt5), то есть не полагаетесь на данные напрямую из терминала, вам фактически не нужно полностью загружать всё из MetaTrader 5-API, вам скорее нужны лишь некоторые константы MetaTrader 5, такие как типы позиций, значения таймфреймов и т. д.
Внутри модуля strategytester5 это различие прояснено.
strategytester5/__init__.py
try: import MetaTrader5 as _mt5 MT5_AVAILABLE = True except ImportError: from strategytester5.mt5 import constants as _mt5 print( "MetaTrader5 is not installed.\n" "On Windows, install it with: pip install strategytester5[mt5]\n" "Falling back to bundled MT5 constants." ) MT5_AVAILABLE = False MetaTrader5 = _mt5
Именно это различие позволяет фреймворку работать даже в неподдерживаемых операционных системах Linux и macOS, если ему предоставлены папки истории с правильными данными (как известно, MetaTrader 5 Python API не работает в этих двух ОС).
Это справедливо только если пользователь не запускает фреймворк в режиме MetaTrader 5.
Шаг 03: объект CTrade для каждого инструмента
Поскольку каждый инструмент имеет собственные свойства, отличные от других, нам нужен отдельный экземпляр CTrade для каждого инструмента.
from strategytester5.trade_classes.Trade import CTrade symbols = tester_configs["symbols"] m_trade_objects = { symbol: CTrade( simulator=tester, magic_number=magic_number, filling_type_symbol=symbol, deviation_points=slippage ) for symbol in symbols }
Заключительный шаг: торговая стратегия
Наша стратегия проста: если текущая цена выше простой скользящей средней за 10 баров, мы открываем позицию на продажу, а когда текущая цена ниже той же скользящей средней — открываем противоположную позицию.
def on_tick_multicurrency(symbol: str): m_trade = m_trade_objects[symbol] tick_info = tester.symbol_info_tick(symbol=symbol) if tick_info is None: # if the process of obtaining ticks wasn't successful return rates_df = None tf = STRING2TIMEFRAME_MAP[timeframe] if is_newbar(tester.current_time, tf): rates = tester.copy_rates_from_pos(symbol=symbol, timeframe=tf, start_pos=0, count=20) rates_df = pd.json_normalize(rates) # a data structure is JSON-like if rates_df.empty: return sma_10 = sma_indicator(close=rates_df["close"], window=10) ask = tick_info.ask bid = tick_info.bid symbol_info = tester.symbol_info(symbol) # symbol information pts = symbol_info.point volume = martingale_lotsize(initial_lot=symbol_info.volume_min, symbol=symbol, current_time=tester.current_time) if ask < sma_10.iloc[-1]: # if price is below the SMA 10 if not pos_exists(magic=magic_number, symbol=symbol, type=mt5.POSITION_TYPE_BUY): # If a position of such kind doesn't exist m_trade.buy(volume=volume, symbol=symbol, price=ask, sl=ask - sl * pts, tp=ask + tp * pts, comment="Tester buy") # we open a buy position if ask > sma_10.iloc[-1]: # if price is above the SMA 10 if not pos_exists(magic=magic_number, symbol=symbol, type=mt5.POSITION_TYPE_SELL): # If a position of such kind doesn't exist m_trade.sell(volume=volume, symbol=symbol, price=bid, sl=bid + sl * pts, tp=bid - tp * pts, comment="Tester sell") # we open a sell position tester.ParallelOnTick(ontick_func=on_tick_multicurrency) # very important!
Для индикатора Simple Moving Average мы используем библиотеку технического анализа (TA).
Для управления капиталом мы используем расчёт размера лота по мартингейлу (чего я не рекомендую).
def martingale_lotsize(initial_lot: float, symbol: str, current_time: datetime, multiplier: float=2) -> float: end_date = datetime.strptime(tester_configs["start_date"], "%d.%m.%Y %H:%M") deals = tester.history_deals_get(date_from=end_date, date_to=current_time) if not deals: return initial_lot last_deal = deals[-1] if last_deal.entry == mt5.DEAL_ENTRY_OUT: # a closed operation if last_deal.profit < 0 and last_deal.symbol == symbol: # if the deal made a loss on the current instrument return last_deal.volume * multiplier return initial_lot
Синтаксис такой же, как при использовании MetaTrader 5 Python API, поскольку наш класс построен поверх него.
Наконец, мы запускаем тестирование стратегии в режиме MetaTrader 5.
Вывод:
StrategyTester Progress on EURUSD: 99%|████████████████████████████████████████████████████████████████████▏| 6124/6193 [00:49<00:00, 107.15bar/s]2026-02-05 10:00:20,899 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112785638400061 closed!815/6193 [00:49<00:02, 176.53bar/s] 2026-02-05 10:00:20,899 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:20,899 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1866.65 equity: 1949.040000 pl: 82.39 StrategyTester Progress on EURUSD: 99%|████████████████████████████████████████████████████████████████████▎| 6136/6193 [00:49<00:00, 109.76bar/s]2026-02-05 10:00:20,928 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112796467200046 opened! StrategyTester Progress on EURUSD: 100%|████████████████████████████████████████████████████████████████████▉| 6187/6193 [00:49<00:00, 162.54bar/s]2026-02-05 10:00:21,182 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 113055667200056 closed!852/6193 [00:49<00:02, 135.75bar/s] 2026-02-05 10:00:21,182 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ████████████████████▏ | 5321/6193 [00:49<00:11, 79.11bar/s] 2026-02-05 10:00:21,182 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1871.61 equity: 1874.360000 pl: 2.75 StrategyTester Progress on EURUSD: 100%|█████████████████████████████████████████████████████████████████████| 6193/6193 [00:49<00:00, 125.16bar/s] 2026-02-05 10:00:21,216 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112796467200046 closed! 2026-02-05 10:00:21,216 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:21,216 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1876.84 equity: 1882.270000 pl: 5.43 2026-02-05 10:00:21,216 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112802918400033 opened! 2026-02-05 10:00:21,498 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112969958400047 closed!882/6193 [00:49<00:02, 132.31bar/s] 2026-02-05 10:00:21,498 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ████████████████████ | 5391/6193 [00:49<00:04, 166.35bar/s] 2026-02-05 10:00:21,498 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1880.28 equity: 1880.590000 pl: 0.31 2026-02-05 10:00:21,498 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Sell order Position: 112990464000031 opened! 2026-02-05 10:00:21,520 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112802918400033 closed!901/6193 [00:49<00:02, 141.29bar/s] 2026-02-05 10:00:21,520 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:21,520 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1872.93 equity: 1864.810000 pl: -8.12 2026-02-05 10:00:21,530 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112830105600007 opened! 2026-02-05 10:00:21,633 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112830105600007 closed!920/6193 [00:49<00:01, 153.08bar/s] 2026-02-05 10:00:21,633 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ████████████████████▎ | 5410/6193 [00:49<00:04, 164.98bar/s] 2026-02-05 10:00:21,633 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1882.85 equity: 1892.970000 pl: 10.12 2026-02-05 10:00:21,633 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112835174400063 opened! 2026-02-05 10:00:22,066 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112835174400063 closed!980/6193 [00:50<00:01, 149.36bar/s] 2026-02-05 10:00:22,066 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => █████████████████████▏ | 5492/6193 [00:50<00:03, 226.76bar/s] 2026-02-05 10:00:22,066 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1873.72 equity: 1864.790000 pl: -8.93 2026-02-05 10:00:22,078 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112869043200053 opened! 2026-02-05 10:00:22,565 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112990464000031 closed!070/6193 [00:50<00:00, 207.80bar/s] 2026-02-05 10:00:22,565 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ██████████████████████▎ | 5588/6193 [00:50<00:03, 178.40bar/s] 2026-02-05 10:00:22,565 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1877.50 equity: 1889.000000 pl: 11.50 2026-02-05 10:00:22,565 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Sell order Position: 113055667200042 opened! 2026-02-05 10:00:22,607 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112869043200053 closed!092/6193 [00:50<00:00, 186.63bar/s] 2026-02-05 10:00:22,607 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:22,607 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1889.28 equity: 1900.220000 pl: 10.94 2026-02-05 10:00:22,615 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112900838400021 opened! 2026-02-05 10:00:22,640 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 113059123200046 opened! 2026-02-05 10:00:22,660 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112900838400021 closed!07/6193 [00:50<00:03, 171.67bar/s] 2026-02-05 10:00:22,662 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:22,662 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1896.20 equity: 1903.400000 pl: 7.20 2026-02-05 10:00:22,688 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112902681600023 opened! StrategyTester Progress on USDCAD: 100%|█████████████████████████████████████████████████████████████████████| 6193/6193 [00:51<00:00, 120.81bar/s] StrategyTester Progress on USDCAD: 100%|████████████████████████████████████████████████████████████████████▋| 6170/6193 [00:51<00:00, 230.27bar/s]2026-02-05 10:00:22,996 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112902681600023 closed!625/6193 [00:51<00:03, 171.11bar/s] 2026-02-05 10:00:22,996 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ██████████████████████▊ | 5643/6193 [00:51<00:04, 122.35bar/s] 2026-02-05 10:00:22,996 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1901.13 equity: 1906.260000 pl: 5.13 2026-02-05 10:00:23,001 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112910054400008 opened! 2026-02-05 10:00:23,266 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112910054400008 closed! 2026-02-05 10:00:23,266 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ███████████████████████▊ | 5725/6193 [00:51<00:02, 231.58bar/s] 2026-02-05 10:00:23,267 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1907.15 equity: 1913.370000 pl: 6.22 2026-02-05 10:00:23,268 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112946227200001 opened! 2026-02-05 10:00:23,547 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112946227200001 closed! 2026-02-05 10:00:23,547 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => █████████████████████████▎ | 5861/6193 [00:51<00:00, 351.41bar/s] 2026-02-05 10:00:23,547 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1912.32 equity: 1917.690000 pl: 5.37 2026-02-05 10:00:23,547 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112986316800034 opened! 2026-02-05 10:00:23,599 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112986316800034 closed! 2026-02-05 10:00:23,599 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:23,599 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1917.13 equity: 1922.140000 pl: 5.01 2026-02-05 10:00:23,599 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112990694400051 opened! 2026-02-05 10:00:23,649 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112990694400051 closed! 2026-02-05 10:00:23,649 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => █████████████████████████▊ | 5903/6193 [00:51<00:00, 369.28bar/s] 2026-02-05 10:00:23,649 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1909.87 equity: 1902.810000 pl: -7.06 2026-02-05 10:00:23,666 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 112996224000033 opened! 2026-02-05 10:00:23,763 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 112996224000033 closed! 2026-02-05 10:00:23,765 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ██████████████████████████▏ | 5945/6193 [00:51<00:00, 381.10bar/s] 2026-02-05 10:00:23,765 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1921.97 equity: 1934.270000 pl: 12.30 2026-02-05 10:00:23,767 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 113016960000039 opened! 2026-02-05 10:00:23,815 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 113016960000039 closed! 2026-02-05 10:00:23,815 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => 2026-02-05 10:00:23,815 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1911.71 equity: 1901.650000 pl: -10.06 2026-02-05 10:00:23,815 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 113022028800004 opened! 2026-02-05 10:00:23,837 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 113022028800004 closed! 2026-02-05 10:00:23,837 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ██████████████████████████▋ | 5989/6193 [00:52<00:00, 392.39bar/s] 2026-02-05 10:00:23,837 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1925.99 equity: 1940.470000 pl: 14.48 2026-02-05 10:00:23,841 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 113023872000040 opened! 2026-02-05 10:00:23,996 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 113023872000040 closed! 2026-02-05 10:00:23,996 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ███████████████████████████▎ | 6036/6193 [00:52<00:00, 406.89bar/s] 2026-02-05 10:00:23,996 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1931.84 equity: 1937.890000 pl: 6.05 2026-02-05 10:00:23,997 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 113051289600041 opened! 2026-02-05 10:00:24,039 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 113051289600041 closed! 2026-02-05 10:00:24,039 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ███████████████████████████▊ | 6081/6193 [00:52<00:00, 416.29bar/s] 2026-02-05 10:00:24,039 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1937.19 equity: 1942.740000 pl: 5.55 2026-02-05 10:00:24,039 | INFO | .mt5 | [tester.py:1252 - order_send() ] => Market Buy order Position: 113055436800050 opened! StrategyTester Progress on GBPUSD: 100%|█████████████████████████████████████████████████████████████████████| 6193/6193 [00:52<00:00, 117.85bar/s] 2026-02-05 10:00:24,282 | INFO | .mt5 | [tester.py:1160 - order_send() ] => Position: 113059584000046 closed! 2026-02-05 10:00:24,282 | DEBUG | .mt5 | [tester.py:1161 - order_send() ] => ████████████████████████████▊| 6178/6193 [00:52<00:00, 433.45bar/s] 2026-02-05 10:00:24,282 | DEBUG | .mt5 | [tester.py:1162 - order_send() ] => balance: 1932.94 equity: 1926.310000 pl: -6.63 2026-02-05 10:00:24,282 | INFO | .mt5 | [tester.py:1450 - __terminate_all_positions() ] => Position 113059584000046 closed successfully! End of test 2026-02-05 10:00:25,932 | INFO | .mt5 | [tester.py:2320 - __GenerateTesterReport() ] => Strategy tester report saved at: Reports/multi-curency-EA-report.html
Вывод.


Заключительные мысли
MetaTrader 5 Python API остаётся лучшим вариантом для получения исторических данных с точки зрения скорости. Если только вы не запускаете этот фреймворк в Linux или macOS, которые теперь поддерживаются, следует всегда полагаться на данные напрямую из терминала: это обеспечивает более высокую производительность и меньшее потребление памяти в сложных программах.
Таблица вложений:
| Имя файла | Описание и использование |
|---|---|
| single_thread.py | Пример мультивалютного торгового робота, который тестирует все инструменты в одном потоке. |
| parallel.py | Пример мультивалютного торгового робота, который тестирует торговую стратегию по разным инструментам в многопоточной среде, где каждый инструмент работает в собственном потоке. |
| tester.json | Содержит важные параметры для класса StrategyTester, напоминающие раздел конфигурации стратегии в MetaTrader 5. |
| requirements.txt | Содержит все зависимости Python, использованные в этом проекте. Версия Python, использованная в этом проекте, — 3.11.0rc2 |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20958
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Алгоритм андского кондора — Andean Condor Algorithm (ACA)
Автоматизация торговых стратегий в MQL5 (Часть 28): Создание гармонического паттерна "Летучая мышь" на основе Price Action с визуальной обратной связью
MetaTrader 5: конструируйте рынок под стратегию — Renko/Range/Volume, синтетика и стресс-тесты на пользовательских символах
Переосмысливаем классические стратегии (Часть 15): Стратегия пробоя диапазона предыдущего дня
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Построение тестера стратегий на Python - это просто эксперимент или есть реальное применение для него?
Здравствуйте, меня очень впечатлил ваш проект. У меня есть один вопрос, потому что мне очень интересно изучить его и, возможно, развивать его дальше как проект с открытым исходным кодом.
В тестере стратегий MetaTrader мы обычно используем советник или индикатор для запуска стратегии. Поэтому мне интересно: где в этом проекте реализован код MQL-стратегии, или здесь вообще нет кода MQL-стратегии?
Есть, поэтому у нас есть вот это: https: //www.mql5.com/en/docs/python_metatrader5.