最終更新: 2026年06月
Photo by Maxim Hopman on Unsplash
MQL5でマルチタイムフレームEAを実装する際には、CopyBuffer()の正しい使い方とリペイント問題の回避が設計の成否を分ける。
単一時間足だけで動くEAを長期間回していると、ある共通の壁にぶつかる。M5足のRSIシグナルだけで売買を判断していると、H1足がレンジ相場のときにも逆張りエントリーを連発して損切りが積み重なる、あの現象だ。筆者もアルゴリズム取引ファンドにいた頃、この問題を「シグナル精度の問題」と思い込んでパラメータ最適化を延々と繰り返した時期がある。どれだけ調整しても成績が安定しなかった。本質はそこじゃなかった。上位時間足の文脈(コンテキスト)を完全に無視してエントリーし続けること自体が、設計レベルの欠陥だったのだ。
マルチタイムフレーム(MTF: Multi-TimeFrame)EAとは、複数の時間足から情報を取得し、上位時間足のトレンド判定と下位時間足のエントリータイミング検出を組み合わせた設計パターンを指す。理論的な有効性は直感的にもわかりやすいが、MQL5での実装には固有の落とし穴がいくつも潜んでいる。特にリペイント問題とPERIOD_CURRENT誤用は、バックテストでは発覚しにくく、本番稼働後に初めてその破壊力を実感するケースが多い。
本稿ではMQL5におけるCopyBuffer()とArraySetAsSeries()の基礎から、HTF(Higher TimeFrame)フィルター+LTF(Lower TimeFrame)エントリー設計の実装パターン、さらに完全なサンプルコードまでを一貫して解説する。
マルチタイムフレーム(MTF)EAとは何か・なぜ有効か
MTF EAとは、上位時間足でトレンド方向を判定し、下位時間足でエントリータイミングを検出するEA設計パターンで、単一時間足設計に比べてトレンドに逆らったエントリーを構造的に排除できる点が最大の利点だ。
トレードの世界では「ダウ理論」や「エリオット波動」の時代から、複数の時間軸を確認することの重要性が語られてきた。これを自動化システムに落とし込んだのがMTF EAだ。
典型的な設計思想はこうなる。
- HTF(例: H1・H4・D1): トレンド方向の判定。移動平均線の傾きやEMAのゴールデン・デッドクロス等を使う
- LTF(例: M5・M15): エントリータイミングの検出。RSI過買い・過売り、ローソク足パターン、ボリンジャーバンドタッチ等
HTFが上昇トレンドを示しているときのみLTFの買いシグナルを有効にする、という構造にすることで、トレンドに逆らったエントリーをフィルタリングできる。
実際にバックテストで単一足設計とMTF設計を比較した際の定性的な観察として、MTF設計のほうがシグナル数は減少するが、プロフィットファクター(PF)が改善する傾向が見られた。ただしこれはパラメータ・通貨ペア・期間に大きく依存するため、「MTFが必ず優れている」という断定は統計的に慎重であるべきだ。肝心なのは設計思想の妥当性ではなく、正しく実装されているかどうかだ。
CopyBuffer + ArraySetAsSeriesの基本実装
MQL5では、テクニカル指標の値を取得するためにCopyBuffer()関数を使う。MT4のiMAOnArray()等とはアーキテクチャが大きく異なり、最初に**ハンドル(整数ID)**を取得し、そのハンドルを経由してデータをコピーする設計になっている。
// ハンドル生成(OnInit()で一度だけ実行する)
int h1_ma_handle = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_SMA, PRICE_CLOSE);
// バッファ用配列の宣言
double ma_buf[];
// 配列を時系列順(インデックス0が最新)に設定
ArraySetAsSeries(ma_buf, true);
// OnTick() 内でのデータ取得
if(CopyBuffer(h1_ma_handle, 0, 0, 2, ma_buf) < 2) return;
CopyBuffer()の引数は(handle, buffer_index, start, count, buffer[])の順だ。start=0, count=2とすることで、最新バーと1本前のバーの2つの値を取得する。
ArraySetAsSeries()が欠かせない理由は、デフォルトでは配列インデックス0が最古データを指すためだ。ArraySetAsSeries(buf, true)を設定することで、インデックス0が最新(直近)のデータを指すようになり、buf[0]が現在のバー値、buf[1]が1本前のバー値という直感的な操作ができる。
戻り値チェックを絶対に省略しないこと。 CopyBuffer()はデータが不足している場合に要求数未満の値を返す。EA起動直後や接続が不安定なときに特に発生しやすく、戻り値が2未満ならreturnして処理を中断するガードはプロダクションコードの必須要件だ。筆者は過去にこのチェックを省略したコードを本番稼働させ、起動直後に不正なエントリーが発生した苦い経験がある。
リペイント問題のメカニズムと完全な回避方法
リペイント(Repaint)問題とは、未確定バーの指標値が時間とともに変化することでバックテストと本番の挙動が乖離する現象であり、MTF EAにおいて最も破壊力の高い実装バグの一つだ。
リペイントのメカニズム
現在進行中の足(未確定足)のOHLCデータや指標値は、そのバーが閉じるまでの間に継続的に変化する。例えばH1足の移動平均値は、H1バーが完成するまでの60分間、刻々と動き続ける。
問題はCopyBuffer(h1_handle, 0, 0, 1, buf)で取得したbuf[0]が、この未確定足の現在値を返す点だ。バックテストエンジンはヒストリカルデータを使うため、各バーの終値しか持っていないことが多い。結果として「バックテストでは常にバー終値を参照した動作」になるが、実際の本番稼働では「未確定足の途中の値を参照した動作」になる。これがバックテストと本番のパフォーマンス乖離の一大原因だ。
完全な回避方法:バー確定チェック
解決策はシンプルで、H1足の新しいバーが確定したときのみ処理を実行するという制約を設けるだけでいい。
// 新バー確定チェック(リペイント防止)
static datetime last_bar = 0;
datetime current_bar = iTime(_Symbol, PERIOD_H1, 0);
if(current_bar == last_bar) return;
last_bar = current_bar;
iTime(_Symbol, PERIOD_H1, 0)はH1足の現在バーの開始時刻を返す。static datetime last_barで前回処理時の開始時刻を保持し、変化があったときのみ以降の処理に進む構造だ。
static変数を使うことで、OnTick()が呼ばれるたびに初期化されず、前回の値が保持される点が重要だ。グローバル変数でも機能するが、ロジックの局所性という観点でstatic変数のほうが設計として明確だ。
もう一つのアプローチとして、buf[1](1本前の確定済みバーの値)のみを参照する方法もある。
// 1本前の確定済みバーを参照する方法
if(CopyBuffer(h1_ma_handle, 0, 0, 3, ma_buf) < 3) return;
bool h1_uptrend = ma_buf[1] > ma_buf[2]; // 確定済みのバーで判定
1バー分の遅延が生じる代わりに、実装がバー確定チェックより単純で同等のリペイント防止効果がある。用途に応じて使い分けるといい。
HTFトレンドフィルター + LTFエントリーの設計パターン
Photo by Austin Hervias on Unsplash
実際のMTF EA設計の骨格を示す。H1足のSMAで大局トレンドを判定し、M5足のRSIで過売り状態を検出して買いエントリーするパターンだ。
// HTFトレンドフィルター + LTFエントリーの骨格
int h1_ma_handle = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_SMA, PRICE_CLOSE);
int m5_rsi_handle = iRSI(_Symbol, PERIOD_M5, 14, PRICE_CLOSE);
double ma_buf[], rsi_buf[];
ArraySetAsSeries(ma_buf, true);
ArraySetAsSeries(rsi_buf, true);
// OnTick() 内
if(CopyBuffer(h1_ma_handle, 0, 0, 2, ma_buf) < 2) return;
if(CopyBuffer(m5_rsi_handle, 0, 0, 2, rsi_buf) < 2) return;
bool h1_uptrend = ma_buf[0] > ma_buf[1]; // H1上昇トレンド
bool m5_oversold = rsi_buf[0] <= 30; // M5過売り
// 新バー確定チェック(リペイント防止)
static datetime last_bar = 0;
datetime current_bar = iTime(_Symbol, PERIOD_H1, 0);
if(current_bar == last_bar) return;
last_bar = current_bar;
h1_uptrendは「現在のH1 SMAが1本前より大きい=上昇中」という単純な定義だ。より洗練させるなら、SMAの傾きを数本分の変化量で計算する、あるいはEMAを使ってラグを減らすといった改良が考えられる。
このコード構造で見落としやすいポイントがある。バー確定チェックをCopyBufferの後に置いていることだ。チェックを先頭に置いてしまうと「H1バーが変わったときだけRSI値も再取得する」という動作になり、M5足のタイムリーなシグナル検出ができなくなる。HTFのフィルター判定はバー確定時に更新し、LTFのエントリーシグナルはOnTick()ごとにチェックするという役割分担を意識した実装が必要だ。
設計パターンとしては次の2通りが現場でよく使われる。
パターンA: HTFはバー確定時更新、LTFはOnTick()毎チェック
OnTick() {
if(H1新バー確定) → HTFフィルター状態を更新してグローバル変数に保持
if(HTFフィルターOK && LTFシグナルON) → エントリー
}
パターンB: 両方ともバー確定時のみ処理
OnTick() {
if(LTF新バー確定) → HTF・LTFを両方チェックしてエントリー判断
}
パターンAはエントリータイミングの精度が高く、パターンBはコードが単純で過剰注文のリスクが低い。スキャルピング系はパターンA、スウィング系はパターンBが向いているケースが多いが、どちらが良いかは結局バックテストで確かめるしかない。
このような多層的なEA設計を自分でコードを書かずに試したい場合は、Claudeと会話しながらインジケータが作れるHedgrow FXが選択肢の一つになる。
PERIOD_CURRENTを使ってはいけない理由
PERIOD_CURRENTは「チャートが表示している時間足」に依存するため、MTF EAに組み込むとチャートの時間足設定を変えるだけでEAの挙動が根本的に変わる設計上の欠陥になる。
MQL5の定数PERIOD_CURRENTは「チャートが表示している時間足」を意味する。一見便利に見えるが、EAの設計に組み込むと重大なリスクを生む。
問題の核心はチャートの時間足設定に依存することだ。
// 危険な実装例
int ma_handle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_SMA, PRICE_CLOSE);
このコードをH1チャートにアタッチすれば「H1の50SMA」を参照する。しかし同じEAをM5チャートにアタッチすれば「M5の50SMA」になる。パラメータを変えていないのに、チャートの時間足を変えるだけでEAの挙動が根本的に変わってしまう。
本番運用でこれが特に問題になるのは、VPS上で複数チャートを管理しているときだ。誤って時間足を変更した状態でEAを再起動すると、意図しないロジックで稼働し続ける。筆者の知人がこれで大きな損失を出した事例を直接見ている。MTF EAのような「どの時間足でどの指標を見るか」が設計の根幹にあるシステムでは、時間足は必ず明示的に固定指定すること。
// 正しい実装
int h1_ma_handle = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_SMA, PRICE_CLOSE);
int m5_rsi_handle = iRSI(_Symbol, PERIOD_M5, 14, PRICE_CLOSE);
さらに、入力パラメータとして時間足を外部から指定可能にするという設計もある。
input ENUM_TIMEFRAMES htf_period = PERIOD_H1; // HTF: デフォルトH1
input ENUM_TIMEFRAMES ltf_period = PERIOD_M5; // LTF: デフォルトM5
int h_htf_ma = iMA(_Symbol, htf_period, 50, 0, MODE_SMA, PRICE_CLOSE);
int h_ltf_rsi = iRSI(_Symbol, ltf_period, 14, PRICE_CLOSE);
ENUM_TIMEFRAMES型を使うことで、MT5の設定画面でドロップダウン選択できる使いやすいインターフェースになる。PERIOD_CURRENTは使わず、デフォルト値として意図する時間足を明示的に書いておくのがベストプラクティスだ。
OnInit()でのハンドル初期化と効率的な実装
MQL5の指標ハンドル生成はコストが高く、OnTick()内で毎回iMA()やiRSI()を呼び出すとティックごとに指標エンジンが新しいバッファを確保・計算するため、ハンドル生成は必ずOnInit()で一度だけ行う必要がある。
MQL5の指標ハンドルは生成コストが高い。iMA()やiRSI()を内部で呼び出すたびに指標エンジンが新しいバッファを確保・計算するためだ。これをOnTick()内で毎回実行すると、ティックごとに膨大なリソースを消費する。
絶対的なルール: ハンドル生成はOnInit()で一度だけ行う。
// グローバル変数でハンドルを保持
int g_h1_ma_handle = INVALID_HANDLE;
int g_m5_rsi_handle = INVALID_HANDLE;
int OnInit()
{
// ハンドルを一括生成
g_h1_ma_handle = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_SMA, PRICE_CLOSE);
g_m5_rsi_handle = iRSI(_Symbol, PERIOD_M5, 14, PRICE_CLOSE);
// ハンドル生成失敗チェック
if(g_h1_ma_handle == INVALID_HANDLE || g_m5_rsi_handle == INVALID_HANDLE)
{
Print("ハンドル生成失敗: ", GetLastError());
return(INIT_FAILED); // EAの初期化を失敗させる
}
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
// ハンドルを明示的に解放
if(g_h1_ma_handle != INVALID_HANDLE) IndicatorRelease(g_h1_ma_handle);
if(g_m5_rsi_handle != INVALID_HANDLE) IndicatorRelease(g_m5_rsi_handle);
}
OnDeinit()でのIndicatorRelease()も地味に重要だ。EAがデタッチされたりターミナルが再起動するときに、確保したリソースを明示的に解放する。省略してもMT5が自動的に解放するケースが多いが、複数のEAが同時稼働している環境では明示的な解放がメモリリークのリスクを下げる。
OnInit()でINIT_FAILEDを返す設計判断も侮れない。ハンドル生成が失敗した状態でEAが稼働し続けると、すべてのCopyBuffer()が失敗し続けながらも、ガード条件次第では意図しない挙動になり得る。初期化失敗を明示的に報告してEAを停止させるほうが安全だ。
完全なMTF EAのサンプルコード(注釈付き)
以上の要素をすべて統合した、実際に動作する完全なサンプルコードを示す。このコードはH1 SMAフィルター+M5 RSIエントリーの買い専用シンプル版だ。実運用ではリスク管理・ポジション管理・決済ロジックを追加する必要がある。
//+------------------------------------------------------------------+
//| MTF_EA_Sample.mq5 |
//| H1 SMAフィルター + M5 RSI 買いエントリー(サンプル用途) |
//+------------------------------------------------------------------+
#property copyright "Sample"
#property version "1.00"
#include <Trade\Trade.mqh>
// 入力パラメータ
input ENUM_TIMEFRAMES InpHTFPeriod = PERIOD_H1; // HTF: 上位時間足
input ENUM_TIMEFRAMES InpLTFPeriod = PERIOD_M5; // LTF: 下位時間足(エントリー)
input int InpMAPeriod = 50; // SMA期間(HTF)
input int InpRSIPeriod = 14; // RSI期間(LTF)
input double InpRSIOversold = 30.0; // RSI過売り閾値
input double InpLotSize = 0.01; // ロットサイズ
input int InpSlippage = 10; // スリッページ許容値(ポイント)
// グローバル変数
int g_htf_ma_handle = INVALID_HANDLE;
int g_ltf_rsi_handle = INVALID_HANDLE;
datetime g_last_htf_bar = 0;
bool g_htf_uptrend = false;
CTrade g_trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// ハンドル生成(OnInit()で一度だけ)
g_htf_ma_handle = iMA(_Symbol, InpHTFPeriod, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
g_ltf_rsi_handle = iRSI(_Symbol, InpLTFPeriod, InpRSIPeriod, PRICE_CLOSE);
// ハンドル検証
if(g_htf_ma_handle == INVALID_HANDLE)
{
Print("HTF MAハンドル生成失敗。エラーコード: ", GetLastError());
return(INIT_FAILED);
}
if(g_ltf_rsi_handle == INVALID_HANDLE)
{
Print("LTF RSIハンドル生成失敗。エラーコード: ", GetLastError());
return(INIT_FAILED);
}
// CTradeの設定
g_trade.SetDeviationInPoints(InpSlippage);
Print("MTF EA 初期化完了。HTF=", EnumToString(InpHTFPeriod),
" LTF=", EnumToString(InpLTFPeriod));
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// ハンドル解放
if(g_htf_ma_handle != INVALID_HANDLE) IndicatorRelease(g_htf_ma_handle);
if(g_ltf_rsi_handle != INVALID_HANDLE) IndicatorRelease(g_ltf_rsi_handle);
}
//+------------------------------------------------------------------+
//| HTFトレンドフィルター更新(新バー確定時のみ) |
//+------------------------------------------------------------------+
void UpdateHTFFilter()
{
datetime current_htf_bar = iTime(_Symbol, InpHTFPeriod, 0);
// 新しいHTFバーが確定した場合のみ更新(リペイント防止)
if(current_htf_bar == g_last_htf_bar) return;
g_last_htf_bar = current_htf_bar;
double ma_buf[];
ArraySetAsSeries(ma_buf, true);
// 戻り値チェック:2本分取得できなければスキップ
if(CopyBuffer(g_htf_ma_handle, 0, 0, 2, ma_buf) < 2)
{
Print("HTF MAデータ取得失敗");
return;
}
// SMAの方向でトレンド判定
g_htf_uptrend = (ma_buf[0] > ma_buf[1]);
Print("HTF更新: ", EnumToString(InpHTFPeriod),
" SMA=", DoubleToString(ma_buf[0], _Digits),
" Uptrend=", g_htf_uptrend);
}
//+------------------------------------------------------------------+
//| LTFエントリーシグナル検出(OnTick()毎) |
//+------------------------------------------------------------------+
bool CheckLTFEntrySignal()
{
double rsi_buf[];
ArraySetAsSeries(rsi_buf, true);
// 戻り値チェック:2本分取得できなければシグナルなし
if(CopyBuffer(g_ltf_rsi_handle, 0, 0, 2, rsi_buf) < 2) return(false);
// RSIが過売り閾値以下かチェック(現在バーの値を使用)
return(rsi_buf[0] <= InpRSIOversold);
}
//+------------------------------------------------------------------+
//| ポジション保有チェック |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol) return(true);
}
return(false);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Step 1: HTFフィルターを更新(新バー確定時のみ処理される)
UpdateHTFFilter();
// Step 2: 既存ポジションがある場合はエントリーしない
if(HasOpenPosition()) return;
// Step 3: HTFフィルターが上昇トレンドを示していない場合はエントリーしない
if(!g_htf_uptrend) return;
// Step 4: LTFエントリーシグナルを確認
if(!CheckLTFEntrySignal()) return;
// Step 5: 買いエントリー
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(g_trade.Buy(InpLotSize, _Symbol, ask, 0, 0, "MTF_BUY"))
{
Print("買いエントリー: ASK=", DoubleToString(ask, _Digits),
" Lot=", InpLotSize);
}
else
{
Print("エントリー失敗: エラーコード=", GetLastError());
}
}
//+------------------------------------------------------------------+
このサンプルコードについて、いくつか補足しておきたい。
UpdateHTFFilter()の設計について: HTF更新処理をOnTick()内に直接書かず、独立した関数に切り出している。単体テスト的な検証がしやすくなるだけでなく、後でHTFの判定ロジックを差し替えるときの影響範囲が限定される。実際に後から「EMAに変えたい」「傾きで判定したい」という改良をするとき、この分離が効いてくる。
g_htf_uptrendのグローバル変数について: HTFフィルターの状態を関数をまたいで保持するためにグローバル変数を使っている。OnInit()でfalseに初期化されるが、最初のHTFバー更新が来るまでエントリーしないという安全な初期状態になっている。
決済ロジックが含まれていないことについて: このサンプルはエントリーロジックの実装パターンに焦点を絞っている。実運用では必ずストップロス・テイクプロフィット、またはトレーリングストップを実装すること。損切りのない状態でのEA稼働は大きなリスクを伴う。
ちなみにHedgrow FXはMQL5によるEA開発をAIとの対話形式でサポートするツールだ。このサンプルのようなMTF構造をベースにカスタムロジックを試したい場合、Claudeと会話しながらインジケータが作れるHedgrow FXを活用することで実装の試行錯誤を効率化できる。
よくある質問(FAQ)
Q: CopyBuffer()の第3引数(start)を0以外にするのはどんな場面ですか?
A: 過去データを一括取得してローカルで分析する場合に使う。例えばバー確定後に過去20本分のRSI値を取得して統計的な閾値を動的に計算するといった用途だ。リアルタイムエントリー判断ではstart=0, count=2またはcount=3程度で十分なケースがほとんどだ。
Q: HTFとLTFの時間足の組み合わせに推奨はありますか?
A: 4倍以上の差があるペアが現場では多く使われる(M5とH1、M15とH4等)。ただし「この組み合わせが最良だ」という統計的な根拠は通貨ペアや期間によって大きく変わるため、必ずバックテストで自分のシステムに合った組み合わせを検証する必要がある。
Q: ArraySetAsSeries()の設定はCopyBuffer()の前後どちらで行うべきですか?
A: CopyBuffer()の前に設定しておくことが望ましい。ArraySetAsSeries()は配列のメモリアドレスを変更するのではなく、読み取り方向のフラグを変更するものなので、CopyBuffer()の前後いずれでも機能する。ただし可読性と意図の明確化という点で、宣言直後またはOnInit()での設定が自然だ。
Q: OnTick()が非常に高頻度で呼ばれる場合のパフォーマンス最適化はありますか?
A: 計算コストの高い処理(統計計算・文字列操作・ファイルI/O)はOnTick()の外に出す。CopyBuffer()自体はMT5の内部キャッシュを使うため、同一パラメータなら再計算コストは低い。ただし時間足が細かいほどOnTick()の呼び出し頻度が上がるため、LTFをM1にする場合は処理時間のプロファイリングをやっておきたい。
Q: バックテストでリペイントが検出されないのはなぜですか?
A: MT5のStrategy Testerはデフォルトで各バーの終値データのみを使ってシミュレーションするモードがある。「Open prices only」モードでは未確定足の途中の動きを再現しないため、リペイント問題が顕在化しない。「Every tick」モードでも詳細なティックシミュレーションを行うが、ヒストリカルデータの品質によっては実際のリアルタイム挙動と差が出る。最終確認はデモ口座でのフォワードテストで行うことを強く勧める。
Q: MQL5での非同期処理やイベント駆動設計はありますか?
A: MQL5にはOnChartEvent()(チャートイベント)やOnTimer()(タイマーイベント)がある。OnTimer()を使えば特定の時間間隔でHTFフィルターを更新するという設計も可能だ。ただし複数のイベントハンドラが絡む実装は状態管理が複雑になるため、まずはOnTick()ベースの設計でシステムを安定させてから手を広げるほうが無難だ。EAのロジック設計をAIと対話しながら進めたい場合は、Claudeと会話しながらインジケータが作れるHedgrow FXが選択肢になる。
Q: MQL5のMTF EA開発をAIツールで効率化する方法はありますか?
A: HTFフィルターの判定ロジック変更やLTFシグナルの追加など、MTF EAの改良は試行錯誤の繰り返しになることが多い。Claudeと会話しながらインジケータが作れるHedgrow FXは、MQL5コードの設計・修正・デバッグをAIとの対話形式で進められるツールで、このサンプルのような構造をベースにカスタムロジックを素早く検証したい場面で活用できる。
免責事項
本記事はMQL5プログラミングの技術情報を提供するものであり、特定の金融商品への投資を推奨するものではありません。FX取引には元本割れのリスクがあります。投資判断はご自身の責任において行ってください。
著者: [著者名プレースホルダー] | アルゴリズム取引・MQL5開発
