Python-MetaTrader 5ストラテジーテスター(第5回):複数銘柄および複数時間足ストラテジーテスター
内容
- はじめに
- 複数時間足データの処理
- 複数時間足アプローチの問題点
- MetaTrader 5からの直接データ抽出
- マルチスレッドによる銘柄処理
- パフォーマンス問題への追加対応
- フル機能の複数通貨自動売買ロボット
- 結論
はじめに
前回の記事では、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つの独立した関数が実装されています。
- _fetch_bars_worker:bars.py内のfetch_historical_bars関数を呼び出し、MetaTrader 5ターミナルからバー情報を取得します。取得したデータは指定された保存先に格納され、Barsというサブフォルダ内に保存されます。
- _fetch_ticks_worker:指定された期間におけるティックデータをMetaTrader 5ターミナルから取得するfetch_historical_ticks関数を呼び出します。取得結果はPolars DataFrameとして整理され、Ticksサブフォルダ配下の任意の保存先に保存されます。
- _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つのアプローチが考えられます。
- すべてのデータ(ティックおよびバー)をMetaTrader 5ターミナルから直接取得する
- 銘柄ごとの並列処理を導入する
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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5標準ライブラリエクスプローラー(第8回):CFileTxtによるハイブリッド取引ジャーナルの記録
MQL5標準ライブラリエクスプローラー(第7回):CCanvasによるインタラクティブなポジションラベル表示
プライスアクション分析ツールキットの開発(第59回):幾何学的非対称性を用いたフラクタル保ち合いからの高精度ブレイクアウトの識別
カスタムインジケータワークショップ(第2回):MQL5で実用的なSupertrend EAを構築する
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
「Pythonで戦略テスター を作る」というのは単なる実験に過ぎないのでしょうか、それとも実用的な用途はあるのでしょうか?
こんにちは。あなたのプロジェクトに本当に感銘を受けました。このプロジェクトを詳しく研究し、将来的にはオープンソースプロジェクトとしてさらに発展させたいと考えているため、一つ質問があります。
MetaTraderのストラテジーテスター では、通常、エキスパートアドバイザーやインジケーターを使ってストラテジーを実行します。そこでお伺いしたいのですが、このプロジェクトではMQLのストラテジーコードはどこに実装されているのでしょうか?それとも、そもそもMQLのストラテジーコードは存在しないのでしょうか?
ありますよ。だからこそ、こんなページがあるんです:https://www.mql5.com/ja/docs/python_metatrader5