移動平均線(MA)ゴールデンクロス&デッドクロスEAを作って検証してみた
ゴールデンクロス・デッドクロスは本当に勝てるのか問題
トレードの世界において最も教科書通りと言っていい代表的なロジックに、異なる期間の複数の移動平均線の交差(クロス)を利用したものがあります。
エントリ条件は、短期MAが長期MAを超えた時にロング(ゴールデンクロス)し、逆に短期MAが長期MAを割り込んだ時にショート(デッドクロス)する形です。
決済条件は様々で、以下のようなバリエーションがあります。
- ロングの時はデッドクロス、ショートの時はゴールデンクロスで決済(エントリ時の逆)
- 固定pips指定で決済
- ボラティリティに応じた変動pips指定で決済
- 短期/長期MAの接線のどちらかが平行になったら決済
- ローソク足が長期MAにタッチまたはクロスしたら決済
- その他もろもろ
利用する移動平均線の数も2本タイプから、3本タイプ、マルチタイプ(いわゆるGMMA)まで、細かいやり方の違いは数え切れない程あります。
今回は数多あるMAクロス流派の中で最もシンプルなものの一つ、2本の移動平均を使い、確定ローソク足の終値が長期MAをクロスした時に決済するEA(MACrosser.mq5)を作ってみました。その後、移動平均線の種類と期間を細かく変えて、短期線/長期線のどの組み合わせが一番勝ちやすいのかを検証してみます。
MACrosser.mq5 コード
作ったコードはこちら。95%ChatGPT産です。コメントに一つ一つの処理の細かい説明を付けてます。
//+------------------------------------------------------------------+
//| MACrosser.mq5 |
//| Copyright 2024, MetaQuotes Software Corp. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property script_show_inputs
#include <Trade\Trade.mqh>
// 入力パラメータの定義
input int ShortMAPeriod = 10; // 短期移動平均の期間
input int LongMAPeriod = 20; // 長期移動平均の期間
input double LotSize = 0.1; // ロットサイズ
input ENUM_MA_METHOD ShortMAMethod = MODE_EMA; // 短期移動平均の方法
input ENUM_MA_METHOD LongMAMethod = MODE_SMA; // 長期移動平均の方法
// グローバル変数の定義
double shortMA[], longMA[]; // 移動平均値を格納する配列
int shortMAHandle, longMAHandle; // 移動平均インジケータのハンドル
CTrade trade; // トレード用オブジェクト
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// 移動平均インジケータのハンドルを作成
shortMAHandle = iMA(_Symbol, _Period, ShortMAPeriod, 0, ShortMAMethod, PRICE_CLOSE);
longMAHandle = iMA(_Symbol, _Period, LongMAPeriod, 0, LongMAMethod, PRICE_CLOSE);
// インジケータハンドルの作成に失敗した場合のエラーチェック
if (shortMAHandle < 0 || longMAHandle < 0)
{
Print("Error creating indicator handles");
return(INIT_FAILED); // 初期化失敗を返す
}
// 配列を時系列順に設定(最新のデータが最初に来るようにする)
ArraySetAsSeries(shortMA, true);
ArraySetAsSeries(longMA, true);
return(INIT_SUCCEEDED); // 初期化成功を返す
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// インジケータハンドルの解放
if (shortMAHandle >= 0) IndicatorRelease(shortMAHandle);
if (longMAHandle >= 0) IndicatorRelease(longMAHandle);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//バックテスト用処理
//短期線と長期線の設定期間が逆になってしまうパターンはノーエントリ
if (ShortMAPeriod >= LongMAPeriod)
return;
static datetime lastTime = 0; // 最後に処理した時間を記録する変数
if (lastTime == iTime(_Symbol, _Period, 0))
return; // 最後のティックと同じ時間の場合は何もしない
lastTime = iTime(_Symbol, _Period, 0); // 最後の時間を更新
// インジケータのバッファから直近3期間(進行中の足、1本前の確定足、2本前の確定足)の値を取得
CopyBuffer(shortMAHandle, 0, 0, 3, shortMA);
CopyBuffer(longMAHandle, 0, 0, 3, longMA);
// 1本前と2本前の移動平均値を取得
double shortMA1 = shortMA[1];
double shortMA2 = shortMA[2];
double longMA1 = longMA[1];
double longMA2 = longMA[2];
double closePrice1 = iClose(_Symbol, _Period, 1); // 1本前の終値を取得
// 移動平均線のクロスを判定
bool crossUp = (shortMA1 > longMA1) && (shortMA2 < longMA2); // ゴールデンクロス
bool crossDown = (shortMA1 < longMA1) && (shortMA2 > longMA2); // デッドクロス
int total = PositionsTotal(); // 現在のポジション数を取得
// 各ポジションをチェックして終了条件を満たす場合はクローズ
for (int i = total - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i); // ポジションのチケット番号を取得
PositionSelectByTicket(ticket); // チケット番号でポジションを選択
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // ポジションのタイプを取得
// ロング時、ローソク足確定終値が長期線を割り込んだ場合
if (type == POSITION_TYPE_BUY && closePrice1 < longMA1)
{
if (!trade.PositionClose(ticket)) // ポジションをクローズ
Print("Error closing long position: ", trade.ResultRetcode());
}
// ショート時、ローソク足確定終値が長期線を超えた場合
else if (type == POSITION_TYPE_SELL && closePrice1 > longMA1)
{
if (!trade.PositionClose(ticket)) // ポジションをクローズ
Print("Error closing short position: ", trade.ResultRetcode());
}
}
// 新しいポジションを開く条件をチェック
if (total == 0 && crossUp) // ゴールデンクロスでロングポジションを開く
{
if (!trade.Buy(LotSize, _Symbol)) // ロングポジションを開く
Print("Error opening buy order: ", trade.ResultRetcode());
}
else if (total == 0 && crossDown) // デッドクロスでショートポジションを開く
{
if (!trade.Sell(LotSize, _Symbol)) // ショートポジションを開く
Print("Error opening sell order: ", trade.ResultRetcode());
}
}
MAの種類は以下の4種類から選択します。「移動平均の方法」の設定項目で、"MODE_"+"移動平均タイプの略称"で指定。SMAから順に下になるにつれて、直近の値をより重視する移動平均タイプになります。
- 単純移動平均 (SMA: Simple Moving Average)
- 平滑移動平均 (SMMA: Smoothed Moving Average)
- 指数移動平均 (EMA: Exponential Moving Average)
- 線形加重移動平均 (LWMA: Linear Weighted Moving Average)
エグジット条件は、検証時間足のローソク足が確定した時の終値と、その時の長期線の値を基準にしています。ロングポジションならローソク足終値が長期線を下回った時、ショートポジションなら長期線を上回った時に決済します。
MACrosser USDJPY 5分足 25,280パターン検証
MACrosserを以下の条件で検証してみました。
- ドル円
- 5分足
- 2020年1月1日~2024年6月8日
- 初期残高100万円スタート
- 毎エントリ0.1ロット固定
- 短期線5~100/長期線10~400(期間5刻み)
- 移動平均4種類(SMA、SMMA、EMA、LWMA)の組み合わせ
- (データ元:Axiory/ヒストリー品質スコア99%)


MACrosser検証結果

上のグラフは全検証パターンを最終残高毎にプロットしたものです。7割くらいは赤の負けパターン。2割が微益(黄緑)、残りの1割(緑/紺)がそこそこ良い成績となりました。

上は成績ベスト30のリストです。短期線SMMA70~100/長期線SMA300~400程度の組み合わせが多め。長期線側は全てSMAになっています。

逆に悪い方のワースト30のリスト。加重平均系を使った短期線5~20/長期線25~70付近の組み合わせが全くダメでした。短期/長期ともに直近の値を重視し過ぎると、短期間に何度も交差するレンジ展開が増えてバランスが悪くなるのかもしれません。
MACrosser:SMMA95/SMA350の分析
続いて一番成績の良かった短期線SMMA95/長期線SMA350のパターンを再検証し、さらに結果を掘り下げて分析してみます。
残高増減

残高変化の様子です。スタートから数トレードは多少揉み合って若干初期資金100万円を割り込みましたが、その後は持ち直して順調に上がり続けています。2021年だけは伸びが頭打ちになって低空飛行でしたが、2022年以降は良い感じに利益が積み重なり、多少落ち込む時期はあったものの、2024年6月現在も悪くなく、最終的には1,358,137円となりました。近年の大きなトレンドである円安相場におおむね相関している感じです。
数値結果(勝率・損益・利回り・PF)

全体の数値結果です。トレード勝率26.9%とかなり悪めですが、損益+358,397円となりました。100万円スタートだと4年5か月で年平均利回り7.18%、月平均利回り0.58%の計算になります。PF(プロフィットファクター)は損失に対する利益の割合を示した値で、PF1.26と悪くない数値です。
勝率悪くとも利益が積み重なっているということは、大きく負けることが少なく、勝つ時はトレンドの波に乗っかって一気に伸びていったということでしょう。勝率よりリスクリワード重視の戦略になっています。
残高最大ドローダウン73,125円と、資金100万円に対してはさほど大きくないため、0.1ロットは少な過ぎたかもしれません。仮にリスクを取って10倍の1ロット固定としたとすると、単純計算で最終残高4,582,905円、損益+3,58,3970円。すると利回りは年平均41.15%、月平均2.91%のかなりの高収益となります。
勝ち/負けパターン分析

ポジション保持時間を見てみると24時間以内が多数を占めており、その中でも大半が負けトレードで、勝っても微益という状況です。クロスしてもレンジ展開となってすぐ負けてしまったことが伺えます。うまくトレンドに乗っかり、保持時間を24時間以上長くできたポジションが利益を生んでいます。

上のチャートはこのEAの典型的な負けパターンを示したものです。短期線、長期線ともにあまり差がなく、短期間に何度も2本のMAが交差する展開に弱いです。一番右側はデッドクロスでショートエントリし、一旦長期線まで戻って損切になった後、良い感じでトレンドになって伸びていきました。損切されてから伸びていくFXあるあるにもハマっています。

もう一つの負けパターンは、大型指標の影響で大きく伸びた後に高値安値でエントリしてしまい、その後の急な戻しにやられてしまう形です(いわゆる”行って来い”)。ローソク足の動きに対して移動平均が後追いになるため、エントリが遅れがちになるために起こります。

一方、勝っている時のチャートです。途中のデッドクロスでショートしたものが即負けになっていますが、その後の再上昇にもすぐに乗っかることができて、キレイなトレンドを全体的にうまく拾えています。一番左のトレードはローソク足が何度か長期線にタッチしていますが、確定終値でなく最安値でのクロスのため決済されず生き残っています。
時間傾向

上は時間単位で区切った取引量と損益のグラフです。気になったのは左下段の時間帯別の「損益(/時間)」。日本時間の朝7~8時、14時~17時、20~21時の時間帯の純利益が突出しています(日本時間GMT+9なのでグラフの時刻に+6~7)。逆にそれ以外の時間帯はトントンかマイナスです。
ドル円が伸びやすい各市場(東京・ロンドン・ニューヨーク)のオープン前後の時間帯にほぼ重なっているため、その時間帯に絞って稼働させれば成績は向上しそうです。逆に言えば、米国指標発表の多い時間帯や、値動きの曖昧な時間帯は避けた方が良さそうです。
(改良版)MACrosser_TZ.mq5 コード
さて今度はこのMACrosserを改良して、実際にパフォーマンスが高まるか再検証してみます。
このEAにはレンジ展開に弱いという傾向がありましたので、勢いのあるトレンドになりやすい市場オープン前後の時間帯(≒損益/時間グラフで利益が大きく出ている時間帯)にエントリーを絞り込めるように作りました。
ただしNY市場の時間帯はあまり利益が出ていないことからエントリはせず、指定の時間になったらローソク足の形に関わらず全決済する条件を付けています。旧EAは24時間以上保持できたポジションのみが利益の大半で実質スイングトレード気味のEAでしたが、今回の改良版では日跨ぎのポジションはやめる戦略です。
//+------------------------------------------------------------------+
//| MACrosser_TZ.mq5 |
//| Copyright 2024, MetaQuotes Software Corp. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property script_show_inputs
#include <Trade\Trade.mqh>
// 入力パラメータの定義
input int ShortMAPeriod = 95; // 短期移動平均の期間
input int LongMAPeriod = 350; // 長期移動平均の期間
input double LotSize = 0.1; // ロットサイズ
input ENUM_MA_METHOD ShortMAMethod = MODE_SMMA; // 短期移動平均の方法
input ENUM_MA_METHOD LongMAMethod = MODE_SMA; // 長期移動平均の方法
// 東京、ロンドンのエントリ許可時間帯の設定(サーバー時間)
input int tkStartHour = 1; // 東京開始時間
input int tkStartMinute = 0; // 東京開始分
input int tkEndHour = 3; // 東京終了時間
input int tkEndMinute = 0; // 東京終了分
input int ldStartHour = 8; // ロンドン開始時間
input int ldStartMinute = 0; // ロンドン開始分
input int ldEndHour = 11; // ロンドン終了時間
input int ldEndMinute = 0; // ロンドン終了分
input int finalCloseHour = 20; // 最終決済時間
input int finalCloseMinute = 0; // 最終決済分
// グローバル変数の定義
double shortMA[], longMA[]; // 移動平均値を格納する配列
int shortMAHandle, longMAHandle; // 移動平均インジケータのハンドル
CTrade trade; // トレード用オブジェクト
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// 移動平均インジケータのハンドルを作成
shortMAHandle = iMA(_Symbol, _Period, ShortMAPeriod, 0, ShortMAMethod, PRICE_CLOSE);
longMAHandle = iMA(_Symbol, _Period, LongMAPeriod, 0, LongMAMethod, PRICE_CLOSE);
// インジケータハンドルの作成に失敗した場合のエラーチェック
if (shortMAHandle < 0 || longMAHandle < 0)
{
Print("Error creating indicator handles");
return(INIT_FAILED); // 初期化失敗を返す
}
// 配列を時系列順に設定(最新のデータが最初に来るようにする)
ArraySetAsSeries(shortMA, true);
ArraySetAsSeries(longMA, true);
return(INIT_SUCCEEDED); // 初期化成功を返す
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// インジケータハンドルの解放
if (shortMAHandle >= 0) IndicatorRelease(shortMAHandle);
if (longMAHandle >= 0) IndicatorRelease(longMAHandle);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//バックテスト用処理
//短期線と長期線の設定期間が逆になってしまうパターンはノーエントリ
if (ShortMAPeriod >= LongMAPeriod)
return;
static datetime lastTime = 0; // 最後に処理した時間を記録する変数
if (lastTime == iTime(_Symbol, _Period, 0))
return; // 最後のティックと同じ時間の場合は何もしない
lastTime = iTime(_Symbol, _Period, 0); // 最後の時間を更新
// インジケータのバッファから直近3期間(進行中の足・1本前の確定足・2本前の確定足)の値を取得
CopyBuffer(shortMAHandle, 0, 0, 3, shortMA);
CopyBuffer(longMAHandle, 0, 0, 3, longMA);
// 1本前と2本前の移動平均値を取得
double shortMA1 = shortMA[1];
double shortMA2 = shortMA[2];
double longMA1 = longMA[1];
double longMA2 = longMA[2];
double closePrice1 = iClose(_Symbol, _Period, 1); // 1本前の終値を取得
// 移動平均線のクロスを判定
bool crossUp = (shortMA1 > longMA1) && (shortMA2 < longMA2); // ゴールデンクロス
bool crossDown = (shortMA1 < longMA1) && (shortMA2 > longMA2); // デッドクロス
// 取引サーバー時間を取得
MqlDateTime serverTimeStruct;
TimeToStruct(TimeCurrent(), serverTimeStruct);
int total = PositionsTotal(); // 現在のポジション数を取得
// 各ポジションをチェックして終了条件を満たす場合はクローズ
for (int i = total - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i); // ポジションのチケット番号を取得
PositionSelectByTicket(ticket); // チケット番号でポジションを選択
ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // ポジションのタイプを取得
// ロング時、ローソク足確定終値が長期線を割り込んだ場合
if (type == POSITION_TYPE_BUY && closePrice1 < longMA1)
{
if (!trade.PositionClose(ticket)) // ポジションをクローズ
Print("Error closing long position: ", trade.ResultRetcode());
}
// ショート時、ローソク足確定終値が長期線を超えた場合
else if (type == POSITION_TYPE_SELL && closePrice1 > longMA1)
{
if (!trade.PositionClose(ticket)) // ポジションをクローズ
Print("Error closing short position: ", trade.ResultRetcode());
}
// 指定時間以上になったら全決済
else if (serverTimeStruct.hour >= finalCloseHour && serverTimeStruct.min >= finalCloseMinute)
{
if (!trade.PositionClose(ticket))
Print("Error closing position: ", trade.ResultRetcode());
}
}
// 夏時間(サーバー時間GMT+3)の場合、東京の時間帯を1時間調整
int adjustedTkStartHour = IsSummerTime(serverTimeStruct) ? tkStartHour + 1 : tkStartHour;
int adjustedTkEndHour = IsSummerTime(serverTimeStruct) ? tkEndHour + 1 : tkEndHour;
// 現在のサーバー時間が指定された時間帯に該当するかをチェック
if (!IsWithinTradingTime(serverTimeStruct, adjustedTkStartHour, tkStartMinute, adjustedTkEndHour, tkEndMinute) &&
!IsWithinTradingTime(serverTimeStruct, ldStartHour, ldStartMinute, ldEndHour, ldEndMinute))
return;
// 新しいポジションを開く条件をチェック
if (total == 0 && crossUp) // ゴールデンクロスでロングポジションを開く
{
if (!trade.Buy(LotSize, _Symbol)) // ロングポジションを開く
Print("Error opening buy order: ", trade.ResultRetcode());
}
else if (total == 0 && crossDown) // デッドクロスでショートポジションを開く
{
if (!trade.Sell(LotSize, _Symbol)) // ショートポジションを開く
Print("Error opening sell order: ", trade.ResultRetcode());
}
}
//+------------------------------------------------------------------+
//| 現在の時間がトレード時間範囲内かを確認する関数 |
//+------------------------------------------------------------------+
bool IsWithinTradingTime(const MqlDateTime ¤tTime, int startHour, int startMinute, int endHour, int endMinute)
{
int startMinutes = startHour * 60 + startMinute;
int endMinutes = endHour * 60 + endMinute;
int currentMinutes = currentTime.hour * 60 + currentTime.min;
if(startMinutes < endMinutes)
return (currentMinutes >= startMinutes && currentMinutes < endMinutes);
else
return (currentMinutes >= startMinutes || currentMinutes < endMinutes);
}
//+------------------------------------------------------------------+
//| サマータイムを判定する関数 |
//+------------------------------------------------------------------+
bool IsSummerTime(const MqlDateTime &serverTimeStruct)
{
// サーバー時間とGMT時間の差を計算
datetime gmtTime = TimeGMT();
MqlDateTime gmtTimeStruct;
TimeToStruct(gmtTime, gmtTimeStruct);
int serverGMTOffsetHours = serverTimeStruct.hour - gmtTimeStruct.hour;
// 年を跨ぐ場合の調整
if(serverTimeStruct.day_of_year != gmtTimeStruct.day_of_year) {
serverGMTOffsetHours += (serverTimeStruct.day_of_year > gmtTimeStruct.day_of_year) ? 24 : -24;
}
// キプロス標準時間のオフセット
int eetOffset = 2; // EETはGMT+2
int eestOffset = 3; // EESTはGMT+3
// サマータイムかどうかを判定
return (serverGMTOffsetHours == eestOffset);
}
MACrosser_TZ:SMMA95/SMA350の分析
改良前のEAで一番成績の良かったSMMA95/SMA350のパターンで検証します。
エントリーと決済の時間は、日本時間(GMT+9)表記で
- (夏)エントリ:8~10時/14~17時 決済:深夜2時
- (冬)エントリ:8~10時/15~18時 決済:深夜3時
と設定しました。米国金利政策発表(FOMC)の影響を避けるため、決済はその発表前の時間帯を意識しています。
数値結果・グラフ
結果は以下の通り。

重要な項目の変化は以下の通り。
- 取引数:1,000 ⇒ 162
- 総損益:+358,397円 ⇒ +104,121円(初期資金1,000,000円/0.1ロット固定)
- 年平均利回り:7.18% ⇒ 2.27%
- 月平均利回り:0.58% ⇒ 0.19%
- 勝率:26.90% ⇒ 32.72%
- PF:1.26 ⇒ 1.60
数値的には、取引数が1/6程度大幅に減りました。合わせて総損益と利回りも減っていますが、勝率32.72%に上昇、PF1.60に改善と、EA自体の効率は良くなっています。無駄なエントリーが減った印象です。
グラフを見る限り、サーバー時間20時の利益が突出していることから、利益が出ているポジションは指定時間での決済が多そうです。日本時間の日中にポジションを持ち、翌深夜2~3時に決済するという、割とシンプルなデイトレードEAに様変わりしました。微損多めですが、日中のトレンドが起こった際に良い感じで乗ってくれるようです。
さらなる検証・改善のアイディア
今回はこれ以上深堀しませんが、まだ検証すべき点は残っています。例えば
- エントリ許可時間帯を変える
- 決済時間を変える
- 決済時間を複数設定する
といった変化をつければ、より最適なエントリ/決済条件が見つかるかもしれません。
また今回はドル円の5分足でやりましたが、
- 銘柄を変える
- 時間足を変える
といったアプローチで、また別の最適値が出て来るはずです。ドルストレート通貨またはクロス円通貨毎の最適値を調べ上げれば、ドル円よりもパフォーマンスの高い銘柄や時間足も見つかるかもしれません。2022年・2023年の相場の動きを考えると、クロス円やゴールドドルあたりが狙い目な感じがします。
ただ毎月安定して収益が上がるEAではないため、このまま運用するにはリスクが高く、モチベーション的にもこのままの形では運用するのは難しそうです。ドルまたは円が大きく動く日を狙って稼働できれば理想的です。前日の株価や債券市場との相関や、取引量あたりをエントリ/決済条件に加えると、面白いかもしれません。日中仕込み型のデイトレードトレンドフォローEAとして、そこそこ可能性はありそうです。
まとめ
今回やったことの一連の流れです。
- 移動平均線2本のゴールデンクロス・デッドクロスロジックEAをChatGPTベースでサクッと作る
- 複数パターンで検証し、勝てる可能性のある移動平均線の組み合わせを探る
- 最も成績の良い組み合わせを再検証する
- 勝ち負けの理由を分析し、改善点を見つける
- EAを改良して再検証する
- 以後、分析⇔検証を繰り返し、収益性の高いEAに練り上げる
およそこんな感じでChatGPTを活用しつつEA開発研究を進めております。自動売買に興味ある皆様の何かしら参考になれば幸いです。




ディスカッション
コメント一覧
まだ、コメントがありません