English Русский Deutsch
preview
Python-MetaTrader 5ストラテジーテスター(第5回):複数銘柄および複数時間足ストラテジーテスター

Python-MetaTrader 5ストラテジーテスター(第5回):複数銘柄および複数時間足ストラテジーテスター

MetaTrader 5トレーディングシステム |
26 5
Omega J Msigwa
Omega J Msigwa

内容


はじめに

前回の記事では、MetaTrader 5のカスタムストラテジーテスターにおいて、リアルティック、生成ティック、そしてターミナルから取得したバーを活用することができました。しかし、カスタムストラテジーテスターの最初の動作確認には成功したものの、複数の銘柄および時間足にまたがるデータの完全な処理・統合はまだ実現できていません(これはMetaTrader 5標準のストラテジーテスターが非常に得意とする領域です)。

仮に複数通貨・複数時間足の自動売買ロボットを持っているとします。

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

ストラテジーテスターの出力:

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の1時間足でストラテジーテスターを起動したにもかかわらず、ストラテジーテスターは依然としてさまざまな銘柄および時間足にわたる、ユーザーが最初に指定した範囲を超えるデータも把握していました。

このターミナルの興味深い特性こそが、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.")

このクラスには、末尾が_workerで終わる3つの独立した関数が実装されています。 

  1. _fetch_bars_worker:bars.py内のfetch_historical_bars関数を呼び出し、MetaTrader 5ターミナルからバー情報を取得します。取得したデータは指定された保存先に格納され、Barsというサブフォルダ内に保存されます。
  2. _fetch_ticks_worker:指定された期間におけるティックデータをMetaTrader 5ターミナルから取得するfetch_historical_ticks関数を呼び出します。取得結果はPolars DataFrameとして整理され、Ticksサブフォルダ配下の任意の保存先に保存されます。
  3. _gen_ticks_worker:  TicksGenクラスのgenerate_ticks_from_barsメソッドを呼び出す役割を持ちます。このクラスは、1分足を基に合成ティックを生成するための処理を担当します。

これら3つの関数は、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から全時間足のバーを同期し、後続処理で利用可能な形でローカルに保存することにあります。これにより、後のシミュレーター内で「CopyRates」系の処理を呼び出す際にも、即座にデータへアクセスできる状態が維持されます。

すべてのバー情報は常時アクセス可能である必要があり、これは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分足、1時間足、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モデリングタイプで実行すると、ストラテジーテスターの処理速度は著しく低下します。

その結果、1秒間に13ティック程度の速度となり、単一のテストであっても完了までに数週間を要する可能性があります。

この問題の原因は、本プロジェクトにおけるデータ管理の構造にあります。ユーザーがバーやティック情報を要求するたびに、Parquetファイルから直接データを読み込む設計となっているためです。一般的に知られている通り、I/O処理はほとんどのプログラミング言語において最も遅い処理の一つです。

理想的には、すべてのデータをメモリ上に保持し、そこから直接参照する設計が望まれます。しかしこのアプローチには別の課題があります。それはデータ量の巨大さです。ティックおよびバー情報を常時すべてメモリに保持することは、現実的に信頼性の高い方法とは限りません。

この問題に対しては、現時点で以下の2つのアプローチが考えられます。

  1. すべてのデータ(ティックおよびバー)をMetaTrader 5ターミナルから直接取得する  
  2. 銘柄ごとの並列処理を導入する


MetaTrader 5からの直接データ抽出

第2回では、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ティック近くまで改善は見られたものの、依然として十分とは言えません。短い期間であっても大量のティックが存在し得ることを考慮すると、この速度では現実的なバックテスト実行時間には到達しません。

たとえば、2025年1月1日から12月31日までの1年間には約1100万ティックが存在します。このペースでは、単一のテスト完了に非常に長い時間を要することになります。


マルチスレッドによる銘柄処理

複数通貨取引システムにおいては、口座残高、証拠金、有効証拠金などのアカウントリソースはすべての銘柄間で共有されます。一方で、取引処理そのものは各銘柄ごとに独立しています。 

この前提を踏まえることで、StrategyTesterクラス内のOnTick関数の設計を見直すことが可能になります。

当初の実装では、利用可能なすべての銘柄・ティックを順番にループ処理し、そのたびにユーザー定義の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関数に銘柄引数を必ず含めたうえで、上記の関数へ渡す必要があります。

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]

ティック単体の処理速度そのものは各銘柄で変わっていないため、厳密には処理速度の根本原因が解決されたわけではありません。しかし、ティック処理を並列化したことにより、全銘柄のティックが同時進行で処理されるため、全体としてのテスト完了時間は大幅に短縮されます。

この結果、たとえば3つの銘柄をテストする場合でも、理論上は単一銘柄をテストした場合とほぼ同等の時間で完了することが保証されます。

前のセクションでは、カスタム履歴を使用するよりも、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]


パフォーマンスの問題へのさらなる対処

ストラテジーテスター5モジュールを用いてバックテストを実行する際、トレーディングロボットの全体的なパフォーマンスを改善するために、まだ実施可能な対策や避けるべき設計上の習慣がいくつか存在します。

A:履歴取得メソッドの不要な呼び出しを避ける

以下のようなStrategyTesterクラスのメソッド:

  • copy_rates_range
  • copy_rates_from_pos
  • copy_rates_from
  • copy_ticks_from
  • copy_ticks_range

これらのメソッドは、Parquetファイルからデータを読み込むためI/O処理が発生し、最も時間消費が大きく計算コストの高い処理の一つとなります。

これらの関数に依存してインジケーター計算などをおこなっている場合、呼び出し頻度をできる限り抑制する必要があります。

前述の例では、copy_rates_from_posを3つの異なる銘柄に対して毎ティック呼び出しており、さらに悪いことに、1ティックあたり4つの異なる時間足に対してループ的に実行されていました。

MQL5ベースのEAでも同様の処理はおこなっていましたが、ターミナル側が非常に高速であったため、計算負荷は問題になりませんでした。一方で本クラスの実装はその水準には達しておらず、さらに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関数は、MQL5プログラミング言語で提供されているPeriodSeconds関数と同様です。モジュールから直接インポートできます。

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分足OHLCモデリングモードは多くのケースにおいて適切な選択肢であり、精度と速度のバランスが最も良い水準に位置しています。

この方式ではループするティック数が大幅に削減されるため(1分足に依存する形となるため)、1年間のテストであっても約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ファイル内で明示的に定義されている必要があります。

このファイルは、import文の直後にスクリプト内で最初に読み込まれます。

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モードでプログラムを実行していない場合、すなわちターミナルから直接データを取得しない構成では、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はこれらのOSでは動作しません)。 

ただし、この前提が成立するのは、ユーザーがフレームワークを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期間の単純移動平均(SMA)より上にある場合には売りポジションを建て、同じ移動平均より下にある場合にはその逆方向のポジションを建てます。

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!

単純移動平均指標については、テクニカル分析ライブラリ(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でも動作するようになった現在では、これらのOSで実行する場合や、複雑なプログラムにおけるメモリ消費を抑えたい場合には、ターミナルから直接データを取得する方式を優先する設計が推奨されます。


以下は添付ファイルの表です。

ファイル名 説明と使用法
single_thread.py 複数銘柄を単一スレッドでテストする複数通貨対応のサンプルEA
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 2月 2026 において 23:04

「Pythonで戦略テスター を作る」というのは単なる実験に過ぎないのでしょうか、それとも実用的な用途はあるのでしょうか?

Omega J Msigwa
Omega J Msigwa | 11 2月 2026 において 05:33
Alain Verleyen 戦略テスター を作る」というのは単なる実験に過ぎないのでしょうか、それとも実用的な用途はあるのでしょうか?
ありますよ。だからこそ、こんなものがあるんです:https://www.mql5.com/ja/docs/python_metatrader5
Mahdi khavari
Mahdi khavari | 29 3月 2026 において 19:14

こんにちは。あなたのプロジェクトに本当に感銘を受けました。このプロジェクトを詳しく研究し、将来的にはオープンソースプロジェクトとしてさらに発展させたいと考えているため、一つ質問があります。

MetaTraderのストラテジーテスター では、通常、エキスパートアドバイザーやインジケーターを使ってストラテジーを実行します。そこでお伺いしたいのですが、このプロジェクトではMQLのストラテジーコードはどこに実装されているのでしょうか?それとも、そもそもMQLのストラテジーコードは存在しないのでしょうか?

Omega J Msigwa
Omega J Msigwa | 3 4月 2026 において 14:04
Mahdi khavari ストラテジーテスター では、通常、エキスパートアドバイザーやインジケーターを使ってストラテジーを実行します。そこでお伺いしたいのですが、このプロジェクトではMQLのストラテジーコードはどこに実装されているのでしょうか?それとも、そもそもMQLのストラテジーコードは存在しないのでしょうか?
これは完全にPythonベースのものです。MQL5でコーディングしたい場合は、MetaEditorとターミナルを使ってテストを行ってください。そのようなシナリオでは、このプロジェクトは役に立ちません。
Alain Verleyen
Alain Verleyen | 3 4月 2026 において 14:33
Omega J Msigwa #:
ありますよ。だからこそ、こんなページがあるんです:https://www.mql5.com/ja/docs/python_metatrader5
それは私の質問に対する答えにはなっていません。
MQL5標準ライブラリエクスプローラー(第8回):CFileTxtによるハイブリッド取引ジャーナルの記録 MQL5標準ライブラリエクスプローラー(第8回):CFileTxtによるハイブリッド取引ジャーナルの記録
本記事では、MQL5標準ライブラリのファイル操作クラスを活用し、Excel対応のCSVファイルを自動生成する堅牢なレポートモジュールを構築します。その過程において、手動操作とアルゴリズム起点の取引アクションを明確に区別し、信頼性が高く監査可能な取引レポートの基盤を整えます。
MQL5標準ライブラリエクスプローラー(第7回):CCanvasによるインタラクティブなポジションラベル表示 MQL5標準ライブラリエクスプローラー(第7回):CCanvasによるインタラクティブなポジションラベル表示
MQL5標準ライブラリに含まれるCCanvasを使用して、ポジション情報を可視化するツールの構築方法を解説します。このプロジェクトを通して、標準ライブラリの各種モジュールを扱うスキルを高めるとともに、ライブチャート上で保有ポジションを視覚的に確認・操作できる実用的なツールを作成します。ぜひ最後までお読みいただき、議論にもご参加ください。
プライスアクション分析ツールキットの開発(第59回):幾何学的非対称性を用いたフラクタル保ち合いからの高精度ブレイクアウトの識別 プライスアクション分析ツールキットの開発(第59回):幾何学的非対称性を用いたフラクタル保ち合いからの高精度ブレイクアウトの識別
様々なブレイクアウト手法を検証する中で、ブレイクアウトの失敗はボラティリティ不足よりも、内部構造の弱さによって引き起こされるケースが圧倒的に多いことに気付きました。この観察結果が、本記事で紹介するフレームワークの出発点となっています。本手法は、最終的な価格スイングが長さ・傾き・速度のすべてにおいて優位性を示すパターンを検出します。これは、方向性を伴う相場拡大に先立ってモメンタムが蓄積されていることを示す明確な兆候です。保ち合い内部に存在するこうした微細な幾何学的不均衡を検出することで、価格がレンジを抜ける前に、より高い確率のブレイクアウトを予測できます。本記事では、このフラクタルベースの幾何学的フレームワークが、どのように構造的不均衡を精度の高いブレイクアウトシグナルへ変換するのかを解説します。
カスタムインジケータワークショップ(第2回):MQL5で実用的なSupertrend EAを構築する カスタムインジケータワークショップ(第2回):MQL5で実用的なSupertrend EAを構築する
Supertrend駆動型エキスパートアドバイザー(EA)をMQL5でゼロから構築する方法を学びます。本記事では、インジケータのリソースとしての組み込み、確定済みバーからのバッファ値の読み取り、確定したフリップの検出、ポジションの整合と切り替え、ストップロス方式およびポジションサイジングの設定について解説します。最後にストラテジーテスターの設定と再現可能なテストを示し、設定可能なEAと、さらなる研究および拡張のための明確なフレームワークを手にすることができます。