English
preview
Тестер стратегий для Python и MetaTrader 5 (Часть 05): Тестер стратегий для нескольких символов и таймфреймов

Тестер стратегий для Python и MetaTrader 5 (Часть 05): Тестер стратегий для нескольких символов и таймфреймов

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

Содержание


Введение

В предыдущей статье мы смогли использовать реальные тики, сгенерированные тики и бары, полученные из терминала 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

  1. _fetch_bars_worker: вызывает функцию fetch_historical_bars из bars.py которая получает данные баров из терминала MetaTrader 5 и сохраняет их в указанном месте, в подпапке с именем Bars
  2. _fetch_ticks_worker: вызывает функцию fetch_historical_ticks, которая получает тики за заданный диапазон дат из терминала MetaTrader 5; результирующий DataFrame Polars сохраняется в нужном месте, в подпапке с именем Ticks
  3. _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-файлов, а, как известно, операции ввода-вывода относятся к самым медленным операциям в большинстве языков программирования.

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

На данный момент мы могли бы решить эту проблему двумя способами :

  1. Извлекать все данные (тики и бары) напрямую из терминала MetaTrader 5  
  2. Организовать параллельную обработку инструментов/символов


Извлечение данных напрямую из терминала 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

Прикрепленные файлы |
Attachments.zip (3.72 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
Alain Verleyen
Alain Verleyen | 10 февр. 2026 в 23:04

Построение тестера стратегий на Python - это просто эксперимент или есть реальное применение для него?

Omega J Msigwa
Omega J Msigwa | 11 февр. 2026 в 05:33
Alain Verleyen тестера стратегий на Python - это просто эксперимент или есть реальное применение для него?
Есть, поэтому у нас есть вот это: https: //www.mql5.com/en/docs/python_metatrader5
Mahdi khavari
Mahdi khavari | 29 мар. 2026 в 19:14

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

В тестере стратегий MetaTrader мы обычно используем советник или индикатор для запуска стратегии. Поэтому мне интересно: где в этом проекте реализован код MQL-стратегии, или здесь вообще нет кода MQL-стратегии?

Omega J Msigwa
Omega J Msigwa | 3 апр. 2026 в 14:04
Mahdi khavari тестере стратегий MetaTrader мы обычно используем советник или индикатор для запуска стратегии. Поэтому мне интересно: где в этом проекте реализован код MQL-стратегии, или здесь вообще нет кода MQL-стратегии?
Проект основан исключительно на Python, если вы хотите кодить на MQL5, используйте MetaEditor и терминал для тестирования, этот проект не имеет смысла в таком сценарии.
Alain Verleyen
Alain Verleyen | 3 апр. 2026 в 14:33
Omega J Msigwa #:
Есть, поэтому у нас есть вот это: https: //www.mql5.com/en/docs/python_metatrader5.
Это не отвечает на мой вопрос.
Алгоритм андского кондора — Andean Condor Algorithm (ACA) Алгоритм андского кондора — Andean Condor Algorithm (ACA)
В статье реализован Andean Condor Algorithm (ACA) для MQL5 — компактный оптимизатор с многомасштабным оператором интенсификации. Выявлен эффект значимого роста качества при малой популяции: одна корректировка настроек выводит его в топ-45 — и за этим стоит характерная особенность алгоритма, о которой стоит знать. Материал даёт готовый код и практические ориентиры по применению.
Автоматизация торговых стратегий в MQL5 (Часть 28): Создание гармонического паттерна "Летучая мышь" на основе Price Action с визуальной обратной связью Автоматизация торговых стратегий в MQL5 (Часть 28): Создание гармонического паттерна "Летучая мышь" на основе Price Action с визуальной обратной связью
В этой статье мы разработаем систему распознавания гармонических паттернов "Летучая мышь" на языке MQL5, которая определяет бычьи и медвежьи гармонические паттерны "Летучая мышь" с использованием пивотных точек и коэффициентов Фибоначчи, запускает сделки с точными уровнями входа, стоп-лосса и тейк-профита. Система также визуализирует паттерны с помощью графических объектов.
MetaTrader 5: конструируйте рынок под стратегию — Renko/Range/Volume, синтетика и стресс-тесты на пользовательских символах MetaTrader 5: конструируйте рынок под стратегию — Renko/Range/Volume, синтетика и стресс-тесты на пользовательских символах
Показываем, как с помощью API пользовательских символов MetaTrader 5 превратить терминал в конструктор данных: генерировать вне‑временные графики Renko, Range и Equal‑Volume и собирать синтетические инструменты. Разбираем агрегацию тиков и модификацию истории для стресс‑тестов (расширение спреда, изменение стоп‑уровней) с учетом ограничений платформы. Даем практику работы с CiCustomSymbol и маршрутизацией приказов на реальный символ через обертку CustomOrder, с готовыми фрагментами кода.
Переосмысливаем классические стратегии (Часть 15): Стратегия пробоя диапазона предыдущего дня Переосмысливаем классические стратегии (Часть 15): Стратегия пробоя диапазона предыдущего дня
Трейдеры-люди уже давно работали на финансовых рынках до появления компьютеров, разработав практические правила, которыми они руководствовались при принятии решений. В этой статье мы вновь рассмотрим хорошо известную стратегию пробоя, чтобы проверить, может ли такая рыночная логика, усвоенная на опыте, конкурировать с систематическими методами. Наши результаты показывают, что, хотя первоначальная стратегия обеспечивала высокую точность, она страдала от нестабильности и слабого контроля рисков. Совершенствуя этот подход, мы продемонстрируем, как инсайты дискреционных трейдеров можно адаптировать в более надежные алгоритмические торговые стратегии.