preview
Python-MetaTrader 5 Strategy Tester (Part 05): Multi-Symbols and Timeframes Strategy Tester

Python-MetaTrader 5 Strategy Tester (Part 05): Multi-Symbols and Timeframes Strategy Tester

MetaTrader 5Trading systems |
301 0
Omega J Msigwa
Omega J Msigwa

Contents


Introduction

In the previous article, we were able to utilize real ticks, generated ticks, and bars obtained from the MetaTrader 5 terminal in our custom strategy tester. Despite ending up with a first successful strategy tester action, we are yet to fully handle and process data across various instruments and timeframes (something the MetaTrader 5  strategy tester does really well).

Suppose you have a multi-currency, multi-timeframe trading robot:

Multicurrency timeframe 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);
         }     
  }

Outputs on the strategy tester:

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

Despite starting the strategy tester for a one-hour timeframe (H1), the strategy tester was still aware of data across various instruments and timeframes, more than what the user asked for initially.

This fascinating ability of the terminal is what makes multicurrency trading systems possible in the MetaTrader 5 terminal.

In this article, we will implement a similar way of handling bars across different instruments and timeframes in our custom MetaTrader 5-Python simulator.


Multi-Timeframe Data Handling

According to the way we designed a custom strategy tester class; during class initialization, we collect all history required during an entire strategy testing action and store them their respective parquet files in a nearby location we can access (by default, in a folder named History. Located under the same path where the main script is found).

Unlike in the previous version of the strategy tester, where we had such code all over the place, this time everything is wrapped in a class named HistoryManager.

To get started, install all dependencies from the requirements.txt file attached at the end of this article, in your Python virtual environment.

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.")

The class has 3 separate functions ending with the name _worker

  1. _fetch_bars_worker: Calls a function fetch_historical_bars from bars.py which fetches bar data from the MetaTrader 5 terminal and stores them in a specified location,  in a subfolder called Bars
  2. _fetch_ticks_worker:Calls a function fetch_historical_ticks which fetches tick in a specified date range from the MetaTrader 5 terminal; stores the resulting Polars DataFrame in a desired location under a subfolder called Ticks
  3. _gen_ticks_worker:  This function calls a method generate_ticks_from_bars  from a class called TicksGen, this class has methods responsible for generating synthetic ticks using bars from the one-minute timeframe.

All these functions are then called upon certain conditions within a function called fetch_history. Unlike the previous version, this time we collect both bars and ticks in a multithreaded environment.

This offers an improvement in terms of speed by reducing the time it took to fetch history for more than 50% compared to previous versions of the custom strategy tester.

Inside StrategyTester's constructor is where we call the function to fetch history.

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,
        )

Shortly after fetching history, we synchronize bars from all timeframes (only when a user is running the program in MetaTrader 5 mode).

        if not self.IS_TESTER:
            hist_manager.synchronize_timeframes()

The goal of synchronizing all timeframes from MetaTrader 5 for all specified symbols (instruments) is to ensure bars across all timeframes are collected and stored in a nearby location for later access, i.e, when a user calls "copying rates" methods within a simulator.

All bar data must be accessible all the time, just like in the MetaTrader 5 terminal.

Multi-symbol configuration:

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"
    }
}

Outputs.

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.

Now, let's try a similar thing in Python as we did in MQL5 before, obtain the current opening price from several timeframes (15 minutes, one hour, 4 hours, daily).

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!

Unlike the MQL5 programming language, Python tends to break the entire program when an exception occurs. To prevent this, we have to implement several if statements that skip the current iteration when no (zero) rates are received from a custom strategy tester.

Notice that we obtained information (opening prices) from all instruments selected in the configuration JSON file.

This is how our custom strategy tester operates, NO SUPRISES. All instruments deployed in the trading bot must be defined initially.

Outputs.

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


The Problem with a Multi-Timeframe Approach

When the above code in a script was run on the real_ticks modelling type, the strategy tester became extremely slow.

Running at around 13 ticks per second, a single test might take weeks to complete.

This is due to the way we structured data management in our project. When a user requests bars or tick information, we read them directly from parquet files, and as we all know, I/O operations are some of the slowest operations in most programming languages.

One of the optimal things we could have done is store all this information in memory and read it from there, but this raises another concern. The massive size of data. It is not always reliable to store all ticks and bars information in memory at once.

We could tackle this problem in two ways for now:

  1. Extracting all data (ticks and bars) directly from the MetaTrader 5 terminal  
  2. Introducing multi-parallelism instrument/symbol handling


Extracting Data Directly from the MetaTrader 5 terminal

In the second article of this series, we introduced a similar syntax to the one provided by the MetaTrader 5-API with an option to choose on whether to rely on the MetaTrader 5 terminal for information or read from a  nearby folder (simulator's history path).

When a final script is called with an argument --mt5, the simulator relies on the MetaTrader 5 API for information (a direct request).

(.env) D:\StrategyTester5\examples\multicurrency trading bot>python bot.py --mt5

When the discussed script in the previous section was run using the MetaTrader 5 mode, there was some improvement in performance (speed).

(.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]

From 13 ticks per second to nearly 600 ticks/s. It's an improvement, still not acceptable considering a large number of ticks that could be available even in a short (time-period).

For instance, 11 million ticks were available in a single year (from 1st January, 2025 to 31st December, 2025), with the current pace, it would still take a very long time to complete a single test.


Multithreaded Instruments Handling

On a multi-currency trading system, all account resources such as the account balance, equity, free margin, etc., are shared through all instruments, but trading operations are isolated. 

With this in mind, we can modify how the OnTick function within the class StrategyTester handles the main (OnTick) function from the user and how it behaves itself.

Initially, we programmed this function to loop through all available ticks in all instruments sequentially. However, this approach has a huge drawback. 

It calls a custom OnTick function from the user on every tick iteration (even on unrelated ticks from other symbols), these unnecessary function calls introduce avoidable computational cost, which slows down the entire program.


Since trading operations are independent from other instruments, we introduce a multithreaded way of handling the OnTick function given by the user.

Each thread hosts trading operations from a single instrument separately, despite sharing the same account resources.


For parallel processing, we need a function for each worker (in this case, for each symbol).

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)

Instead of overwriting the function we made previously, we introduce a new OnTick function for parallel strategy testing.

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()

With this new approach, a user must have a symbol argument in the custom OnTick function before passing it to the above function.

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!

Let's try running a script once more and observe its performance.

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]

Despite the speed it takes to process ticks remaining the same for every instrument as we haven't really addressed an actual cause for a slow strategy testing action operation, handling ticks in parallel reduces the overall time it takes to process all available ticks as ticks across all instruments are processed simultaneously.

In this case, testing three instruments is guaranteed to finish at roughly the same time it would have taken to test a single instrument.

In the previous section, we realized that relying on MetaTrader 5 as a direct source of data offers an improved performance compared to using our custom history. Now, let's try this approach in MetaTrader 5 mode.

(.env) D:\StrategyTester5\examples\multicurrency trading bot>python parallel.py --mt5 

Outputs.

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]


Addressing Performance Issues Further

There are still certain steps we could take and avoid certain habits to improve the overall performance of trading robots as we test them using a StrategyTester5 module, including:

A: Avoid Unnecessary Function Calls for Methods that Fetch History

Methods from the StrategyTester class such as;

  • copy_rates_range
  • copy_rates_from_pos
  • copy_rates_from
  • copy_ticks_from
  • copy_ticks_range

Are the most time-consuming and computationally expensive methods since they read information from parquet files (they introduce I/O operations).

If your trading bot happens to rely on any of these methods for indicator calculations and more, you should prevent calling them very often.

In the previous example, we were calling the method copy_rates_from_pos on every tick from 3 different instruments, and even worse, four times at a single tick (on 4 different timeframes iteratively).

While we also did a similar thing in our MQL5-based EA, and the terminal was super fast, just like this computationally expensive operation was nothing but, our class is nowhere near that level, not to mention Python is a slower programming language.

The best way to prevent unnecessary function calls is to use a new bar event handler.

We'll implement a simple one that relies on the current time and timeframe.

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

The function PeriodSeconds is similar to PeriodSeconds offered in the MQL5 programming language; you can import it directly from the module.

from strategytester5 import PeriodSeconds, TIMEFRAME2STRING_MAP

Now, instead of getting rates (bars data) on every tick (which is a bad idea because information from bars only changes when a new one is introduced), we obtain bars' information only when a new one is detected.

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

Outputs.

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]

There was a significant improvement in the speed of the StrategyTester.

Even better on MetaTrader 5 mode.

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: Choosing the Right Modelling for the Program

Strategy testing based on real ticks provides the most accurate and reliable trading outcome; however, it is computationally expensive and very slow. There is a tradeoff between speed and accuracy.

One must choose wisely the right modelling depending on their program needs. If your strategy doesn't rely on operations at every tick, new bar mode (the least accurate but fastest) might be appropriate for such a program.

The 1-minute OHLC timeframe is suitable for most cases, as it strikes the right balance in accuracy and speed.

With a few ticks to loop through (depending on 1-minute bars), the test took 17 seconds for an entire year.

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]


A Fully Functional Multi-Currency Trading Robot

Now that we understand how multicurrency works in our simulator and the precautions to take when testing one, let's build a fully functional/simple trading robot in Python that trades on several currencies.

Step 01: We need a configuration JSON file

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"
    }
}

Again, all instruments needed in the program must be predefined in a configuration JSON file.

This file is then loaded, the first thing in the script after imports.

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"]

Step 02: We initialize a MetaTrader 5 instance and assign its Object to the StrategyTester class

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")

Note:

You should avoid importing the MetaTrader 5 module directly, always import it from the strategytester5 module.

from strategytester5.tester import StrategyTester, MetaTrader5 as mt5

Unless you are running your programs on a MetaTrader 5 mode (with an argument --mt5) i.e, relying on data directly from the terminal, you don't really need to fully load up everything from the MetaTrader 5-API, you rather need some of MetaTrader 5 constants such as position types, timeframe values, etc,.

Inside a strategytester5 module, the distinction is clarified.

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

This distinction is what enables this framework to work even on unsupported operating systems, Linux and macOS, when given history folders with the right data (as we know the MetaTrader 5 Python APi doesn't operate on these two OS). 

Only if a user doesn't call the framework in MetaTrader 5 mode.

Step 03: A CTrade object for each instrument

Since every instrument has distinct properties from others, we need a different CTrade instance for every instrument.

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
}

Final Step: A trading strategy

Our strategy is simple: if the current price is above a simple moving average of 10 bars, we open a sell position and open an opposite position when the current price is below the same  moving average.

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!

For the Simple Moving Average indicator, we use a technical analysis library (TA).

For money management, we use martingale lot sizing (something I don't recommend).

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

The syntax is the same as using the MetaTrader 5 Python API, as our class is built on top of it.

Finally, we run a strategy tester action onthe MetaTrader 5 mode.

Outputs:

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

Outputs.



Final Thoughts

The MetaTrader 5 Python API reigns superior for providing historical data (in terms of speed), unless you are running this framework on LINUX or macOS, which are now supported, you should always rely on data directly from the terminal, doing so, offers an improved performance and less memory consumption for complex programs.


Attachments Table:

Filename Description & Usage
single_thread.py A multicurrency example trading bot that tests all instruments in a single thread.
parallel.py A multicurrency example trading bot that tests a trading strategy across various instruments in a multithread environment, each instrument in its own thread. 
tester.json Has important configurations for the StrategyTester class, which resembles the MetaTrader 5 strategy configuration section.
requirements.txt It contains all Python dependencies used in this project.

Python version used in this project is 3.11.0rc2
Attached files |
Attachments.zip (3.72 KB)
Overcoming Accessibility Problems in MQL5 Trading Tools (I) Overcoming Accessibility Problems in MQL5 Trading Tools (I)
This article explores an accessibility-focused enhancement that goes beyond default terminal alerts by leveraging MQL5 resource management to deliver contextual voice feedback. Instead of generic tones, the indicator communicates what has occurred and why, allowing traders to understand market events without relying solely on visual observation. This approach is especially valuable for visually impaired traders, but it also benefits busy or multitasking users who prefer hands-free interaction.
Custom Indicator Workshop (Part 2) : Building a Practical Supertrend Expert Advisor in MQL5 Custom Indicator Workshop (Part 2) : Building a Practical Supertrend Expert Advisor in MQL5
Learn how to build a Supertrend-driven Expert Advisor in MQL5 from the ground up. The article covers embedding the indicator as a resource, reading buffer values on closed bars, detecting confirmed flips, aligning and switching positions, and configuring stop-loss modes and position sizing. It concludes with Strategy Tester setup and reproducible tests, leaving you with a configurable EA and a clear framework for further research and extensions.
Angular Analysis of Price Movements: A Hybrid Model for Predicting Financial Markets Angular Analysis of Price Movements: A Hybrid Model for Predicting Financial Markets
What is angular analysis of financial markets? How to use price action angles and machine learning to make accurate forecasts with 67% accuracy? How to combine a regression and classification model with angular features and obtain a working algorithm? What does Gann have to do with it? Why are price movement angles a good indicator for machine learning?
From Basic to Intermediate: Indicator (II) From Basic to Intermediate: Indicator (II)
In this article, we will examine how to implement a moving average calculation and what precautions should be taken when performing this calculation. We will also discuss overloading the OnCalculate function to know when and how to work with one model or another.