AI EA スクール
0記事中 0記事 読了
カリキュラム 第4章 EAを作る MetaEditorで新規EAファイルを作る
4-1

MetaEditorで新規EAファイルを作る

読み終わるまでの目安 約20分

この記事でできるようになること

  • MetaEditorのウィザードで新しいEAファイルを作れる
  • 自動生成されたコードを消して、サンプルEAのコードを貼り付けられる
  • EAファイルがどこに保存されているか分かる

準備するもの

  • MT4(デモ口座でログイン済み)
  • この記事に載せているサンプルEAのコード(RSI_Reversal_Sample.mq4)

この章では、まずスクール共通のサンプルEA「RSI_Reversal_Sample」で練習します。第3章でChatGPTに作ってもらった自分のコードは、サンプルで一度成功したあとに、同じ手順で貼り付けてください。「まず確実に動くもので手順を覚える」ためです。

手順

手順1 MetaEditorを開く

MT4を起動し、キーボードの [F4] を押します。ツールバーの「MetaEditor」アイコンを押しても同じです。要確認

別ウィンドウでMetaEditorが開きます。画面は第2章で確認したとおり、左にファイル一覧(ナビゲータ)、中央にコード、下にメッセージ欄(ツールボックス)という構成です。要確認

手順2 ウィザードで「エキスパートアドバイザ」を選ぶ

MetaEditorの左上「ファイル」→「新規作成」を押します([Ctrl]+[N] でも可)。「MQLウィザード」という画面が開きます。要確認

一覧の中から「エキスパートアドバイザ(テンプレート)」を選んで「次へ」を押します。要確認

似た名前の項目がいくつか並んでいますが、「カスタムインディケータ」「スクリプト」はEAではないので選ばないでください。

手順3 名前をつける

「名前」欄に RSI_Reversal_Sample と入力します。要確認

名前のルールは次のとおりです。

  • 半角英数字とアンダースコア(_)だけを使う(日本語やスペースは避ける)
  • 拡張子(.mq4)は自動で付くので入力しない

「作者」「リンク」欄は空欄でかまいません。下に「パラメータ」を追加する欄がありますが、ここも何も追加せずに「次へ」を押します。要確認

手順4 残りの画面はそのまま「完了」する

続けて「イベントハンドラ」「テスターのイベントハンドラ」といったチェックボックスの画面が出ます。要確認 どれもチェックを変えずに「次へ」→「完了」と進みます。

これから中身をすべて貼り替えるので、ここで何を選んでも結果は同じです。

完了すると、中央にコードがすでに書かれた状態で「RSI_Reversal_Sample.mq4」が開きます。これはMetaEditorが自動で作った「ひな形」です。

手順5 自動生成されたコードをすべて消す

中央のコード部分を1回クリックしてから、[Ctrl]+[A](すべて選択)→ [Delete] を押します。コードが全部消えて、真っ白になればOKです。

「せっかく作られたコードを消していいの?」と思うかもしれませんが、ひな形は「何もしないEA」なので、消して問題ありません。

手順6 サンプルEAのコードを貼り付ける

下のコードが、このスクールの共通教材「RSI_Reversal_Sample.mq4」の全文です。RSIの逆張り(RSIが30を割り込んだら買い、70を抜けたら売り)の、いちばん基本的なEAです。第3章の実例2(RSI逆張りEA)のプロンプトで頼んだ内容とほぼ同じ仕様になっています。なお、MT4に標準で入っている「Moving Average」というEAとは別物です。

コードブロックの右上の「コピー」を押すか、枠内をすべて選択してコピーし、真っ白になったMetaEditorの中央に [Ctrl]+[V] で貼り付けてください。

コードの内容は読まなくて大丈夫です。ただ、input で始まる行の数字(14、30、70、50、30、0.1、12345)が、前の章の仕様シートで決めた値(RSIの期間、買いの水準、売りの水準、利確、損切り、ロット数、マジックナンバー)と対応していることだけ、眺めて確認しておいてください。ここは変えなくてOKです。

コードの右上にある「コピー」ボタンを押すと、全文をまとめてコピーできます。長いので手で選択せず、必ずこのボタンを使ってください。途中で切れるとコンパイルに失敗します。

//+------------------------------------------------------------------+
//|                                         RSI_Reversal_Sample.mq4  |
//|                                    AI EAスクール サンプルEA      |
//|                                                                  |
//|  RSIの逆張りEAです。                                             |
//|    買い: RSIが30を下回ったとき(売られすぎ)                     |
//|    売り: RSIが70を上回ったとき(買われすぎ)                     |
//|    決済: 反対側の水準に到達したとき、または利確・損切り          |
//|                                                                  |
//|  デモ口座で十分に動作を確認してから使ってください。              |
//|  このEAは学習用のサンプルであり、利益を保証するものではありません。|
//+------------------------------------------------------------------+
#property copyright "AI EAスクール"
#property link      "https://ai-school.site-sample.online"
#property version   "1.00"
#property strict
#property description "RSI逆張りの学習用サンプルEA(AI EAスクール)"

//--- 売買ルール -----------------------------------------------------
input int    RSIPeriod     = 14;    // RSIの期間
input double BuyLevel      = 30;    // この値を下回ったら買い(売られすぎ)
input double SellLevel     = 70;    // この値を上回ったら売り(買われすぎ)
input double TakeProfitPips = 50;   // 利確(pips) 0で利確なし
input double StopLossPips   = 30;   // 損切り(pips) 0で損切りなし

//--- 注文の設定 -----------------------------------------------------
input double Lots           = 0.1;   // ロット数
input double SlippagePips   = 1.0;   // 許容スリッページ(pips)
input double MaxSpreadPips  = 3.0;   // このスプレッドを超えたら新規注文しない(pips) 0で無制限
input int    MagicNumber    = 12345; // このEAの注文を見分ける番号

//--- 安全装置 -------------------------------------------------------
input int    MaxRetry       = 3;     // 注文が失敗したときの再試行回数
input int    RetryWaitMs    = 700;   // 再試行までの待ち時間(ミリ秒)
input int    ExpectedTFMin  = 60;    // 想定する時間足(分) 0で確認しない
input bool   ShowLog        = true;  // 「エキスパート」タブに動作ログを出す

//--- 内部で使う値 ---------------------------------------------------
double   g_pip;        // 1pipの価格幅
int      g_slippage;   // スリッページ(ポイント単位)
datetime g_lastBar;    // 最後に判定した足の時刻

//+------------------------------------------------------------------+
//| ログ出力                                                          |
//+------------------------------------------------------------------+
void Log(string msg)
  {
   if(ShowLog) Print(msg);
  }

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 3桁/5桁業者に対応した1pipの値を求める
//    USDJPYが3桁(0.001刻み)なら、1pip = 0.01 = Point * 10
   g_pip = (Digits == 3 || Digits == 5) ? Point * 10 : Point;

//--- スリッページはpipsで受け取り、注文時に使うポイント単位へ直す
   g_slippage = (int)MathRound(SlippagePips * g_pip / Point);
   if(g_slippage < 1) g_slippage = 1;

//--- 起動直後に「過去のシグナル」で発注しないよう、現在の足を記録しておく
//    これを入れないと、EAを乗せた瞬間に1本前のシグナルで発注します
   g_lastBar = Time[0];

//--- 設定の妥当性を確認
   if(RSIPeriod < 2)
     {
      Alert("RSIの期間は2以上にしてください。");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(BuyLevel <= 0 || SellLevel >= 100 || BuyLevel >= SellLevel)
     {
      Alert("買いの水準(" + DoubleToString(BuyLevel, 1) + ")は売りの水準(" +
            DoubleToString(SellLevel, 1) + ")より小さく、0〜100の範囲にしてください。");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(NormalizeLots(Lots) <= 0)
     {
      Alert("ロット数がこの口座で使える範囲にありません。");
      return(INIT_PARAMETERS_INCORRECT);
     }

//--- 想定と違う時間足に乗せていないか知らせる(動作は止めません)
   if(ExpectedTFMin > 0 && Period() != ExpectedTFMin)
      Log("注意: このEAは" + IntegerToString(ExpectedTFMin) + "分足を想定しています。" +
          "いまは" + IntegerToString(Period()) + "分足のチャートに乗っています。");

   Log("RSI_Reversal_Sample v1.00 開始 / " + Symbol() +
       " / RSI(" + IntegerToString(RSIPeriod) + ") " +
       DoubleToString(BuyLevel, 0) + "-" + DoubleToString(SellLevel, 0) +
       " / 1pip=" + DoubleToString(g_pip, Digits));
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| ティックごとの処理                                                 |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- RSIの計算に必要な本数が揃うまで何もしない
//    足りないとiRSIが正しい値を返しません
   if(Bars < RSIPeriod + 3) return;

//--- 確定した足が新しくなったときだけ判定する
   if(Time[0] == g_lastBar) return;
   g_lastBar = Time[0];

//--- 1本前と2本前の確定足でRSIを取る
   double rsiPrev = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 2);
   double rsiCurr = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 1);
   if(rsiPrev <= 0 || rsiCurr <= 0) return;

//--- 逆張りのシグナル
//    売られすぎの水準を「上から下に」割り込んだら買い
   bool buySignal  = (rsiPrev >= BuyLevel  && rsiCurr <  BuyLevel);
//    買われすぎの水準を「下から上に」抜けたら売り
   bool sellSignal = (rsiPrev <= SellLevel && rsiCurr >  SellLevel);

//--- 反対側の水準に届いたら決済する
   bool closeBuy  = (rsiCurr >= SellLevel);   // 買いポジションは買われすぎで手仕舞い
   bool closeSell = (rsiCurr <= BuyLevel);    // 売りポジションは売られすぎで手仕舞い

   if(closeBuy)  CloseMyPositions(OP_BUY);
   if(closeSell) CloseMyPositions(OP_SELL);

//--- ここから新規注文。取引できる状態かを先に確かめる
   if(!buySignal && !sellSignal) return;
   if(CountMyPositions() > 0) return;
   if(!CanTradeNow()) return;

   if(buySignal)
     {
      Log("シグナル: RSIが" + DoubleToString(BuyLevel, 0) + "を下回りました(" +
          DoubleToString(rsiPrev, 1) + " → " + DoubleToString(rsiCurr, 1) + ")");
      OpenOrder(OP_BUY);
     }
   if(sellSignal)
     {
      Log("シグナル: RSIが" + DoubleToString(SellLevel, 0) + "を上回りました(" +
          DoubleToString(rsiPrev, 1) + " → " + DoubleToString(rsiCurr, 1) + ")");
      OpenOrder(OP_SELL);
     }
  }

//+------------------------------------------------------------------+
//| いま注文を出せる状態かを確かめる                                   |
//+------------------------------------------------------------------+
bool CanTradeNow()
  {
   if(!IsConnected())
     {
      Log("見送り: サーバーに接続されていません。");
      return(false);
     }
   if(!IsTradeAllowed())
     {
      Log("見送り: 自動売買が許可されていません。ツールバーの「自動売買」と、" +
          "EAの設定にある「自動売買を許可する」を確認してください。");
      return(false);
     }
   if(IsTradeContextBusy())
     {
      Log("見送り: 別のEAが注文処理中です。");
      return(false);
     }
   if(MaxSpreadPips > 0)
     {
      double spread = MarketInfo(Symbol(), MODE_SPREAD) * Point / g_pip;
      if(spread > MaxSpreadPips)
        {
         Log("見送り: スプレッドが広すぎます(" + DoubleToString(spread, 1) +
             "pips > " + DoubleToString(MaxSpreadPips, 1) + "pips)。");
         return(false);
        }
     }
   return(true);
  }

//+------------------------------------------------------------------+
//| ロット数を口座のルールに合わせる                                   |
//|  最小・最大・刻み幅から外れたロットは error 131 で弾かれます        |
//+------------------------------------------------------------------+
double NormalizeLots(double lots)
  {
   double minLot  = MarketInfo(Symbol(), MODE_MINLOT);
   double maxLot  = MarketInfo(Symbol(), MODE_MAXLOT);
   double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
   if(minLot <= 0)  minLot  = 0.01;
   if(maxLot <= 0)  maxLot  = 100.0;
   if(lotStep <= 0) lotStep = 0.01;

   double v = MathFloor(lots / lotStep + 0.0000001) * lotStep;
   if(v < minLot) v = minLot;
   if(v > maxLot) v = maxLot;

//--- 刻み幅から小数点以下の桁数を求めて丸める
   int digits = 0;
   double s = lotStep;
   while(s < 0.999999 && digits < 8) { s *= 10.0; digits++; }
   return(NormalizeDouble(v, digits));
  }

//+------------------------------------------------------------------+
//| 損切り・利確を置ける最小の距離(価格幅)                           |
//+------------------------------------------------------------------+
double MinStopDistance()
  {
   double stopLevel   = MarketInfo(Symbol(), MODE_STOPLEVEL)   * Point;
   double freezeLevel = MarketInfo(Symbol(), MODE_FREEZELEVEL) * Point;
   double d = MathMax(stopLevel, freezeLevel);
//--- 業者が0を返す場合でも、最低1ポイントは離す
   if(d < Point) d = Point;
   return(d);
  }

//+------------------------------------------------------------------+
//| 再試行して意味のあるエラーかどうか                                 |
//+------------------------------------------------------------------+
bool IsRetryableError(int err)
  {
   switch(err)
     {
      case 4:    // サーバーが混んでいる
      case 6:    // 接続なし
      case 128:  // 取引のタイムアウト
      case 129:  // 価格が正しくない
      case 135:  // 価格が変わった
      case 136:  // 気配値が出ていない
      case 137:  // ブローカーが応答していない
      case 138:  // リクオート
      case 146:  // 取引処理が混んでいる
        return(true);
     }
   return(false);
  }

//+------------------------------------------------------------------+
//| 新規注文(失敗したら再試行する)                                   |
//+------------------------------------------------------------------+
void OpenOrder(int type)
  {
   double lots = NormalizeLots(Lots);

//--- 証拠金が足りるかを先に確認する(足りないと error 134)
   double freeMargin = AccountFreeMarginCheck(Symbol(), type, lots);
   if(freeMargin <= 0 || GetLastError() == 134)
     {
      Log("注文中止: 証拠金が足りません。ロットを小さくするか入金してください。");
      return;
     }

   for(int attempt = 1; attempt <= MaxRetry; attempt++)
     {
      if(!RefreshRates())
        {
         Sleep(RetryWaitMs);
         continue;
        }

      double price = (type == OP_BUY) ? Ask : Bid;
      double sl    = CalcStop(type, price, StopLossPips,   true);
      double tp    = CalcStop(type, price, TakeProfitPips, false);
      color  arrow = (type == OP_BUY) ? clrDodgerBlue : clrOrangeRed;

      int ticket = OrderSend(Symbol(), type, lots, price, g_slippage, sl, tp,
                             "RSI_Reversal_Sample", MagicNumber, 0, arrow);
      if(ticket >= 0)
        {
         Log("注文成功: " + (type == OP_BUY ? "買い" : "売り") +
             " " + DoubleToString(lots, 2) + "ロット @ " + DoubleToString(price, Digits) +
             " / チケット " + IntegerToString(ticket));
         //--- SL/TPが付かなかった場合はあとから付け直す
         if((sl == 0 && StopLossPips > 0) || (tp == 0 && TakeProfitPips > 0))
            AttachStops(ticket, type);
         return;
        }

      int err = GetLastError();

      //--- 130 は損切り・利確が近すぎる。SL/TPなしで出してから付け直す
      if(err == 130)
        {
         Log("損切り・利確が業者の最小距離より近いため、先に注文だけ出します。");
         if(!RefreshRates()) { Sleep(RetryWaitMs); continue; }
         price  = (type == OP_BUY) ? Ask : Bid;
         ticket = OrderSend(Symbol(), type, lots, price, g_slippage, 0, 0,
                            "RSI_Reversal_Sample", MagicNumber, 0, arrow);
         if(ticket >= 0)
           {
            Log("注文成功(損切り・利確はあとで設定): チケット " + IntegerToString(ticket));
            AttachStops(ticket, type);
            return;
           }
         err = GetLastError();
        }

      Log("注文失敗(" + IntegerToString(attempt) + "回目): エラー " + IntegerToString(err) +
          " " + ErrorText(err));

      if(!IsRetryableError(err)) return;   // 直しようのないエラーは繰り返さない
      Sleep(RetryWaitMs);
     }
   Log("注文中止: " + IntegerToString(MaxRetry) + "回試しましたが成立しませんでした。");
  }

//+------------------------------------------------------------------+
//| 損切り・利確の価格を求める(最小距離に届かない場合は押し広げる)   |
//+------------------------------------------------------------------+
double CalcStop(int type, double price, double pips, bool isStopLoss)
  {
   if(pips <= 0) return(0);

   double dist = pips * g_pip;
   double minD = MinStopDistance();
   if(dist < minD) dist = minD;

   double v;
   if(isStopLoss)
      v = (type == OP_BUY) ? price - dist : price + dist;
   else
      v = (type == OP_BUY) ? price + dist : price - dist;

   return(NormalizeDouble(v, Digits));
  }

//+------------------------------------------------------------------+
//| 約定後に損切り・利確を設定する                                     |
//+------------------------------------------------------------------+
void AttachStops(int ticket, int type)
  {
   for(int attempt = 1; attempt <= MaxRetry; attempt++)
     {
      if(!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES))
        {
         Sleep(RetryWaitMs);
         continue;
        }
      RefreshRates();

      double base = OrderOpenPrice();
      double sl   = CalcStop(type, base, StopLossPips,   true);
      double tp   = CalcStop(type, base, TakeProfitPips, false);

      if(sl == 0 && tp == 0) return;
      if(OrderStopLoss() == sl && OrderTakeProfit() == tp) return;

      if(OrderModify(ticket, base, sl, tp, 0, clrGray))
        {
         Log("損切り・利確を設定: SL " + DoubleToString(sl, Digits) +
             " / TP " + DoubleToString(tp, Digits));
         return;
        }

      int err = GetLastError();
      Log("損切り・利確の設定に失敗(" + IntegerToString(attempt) + "回目): エラー " +
          IntegerToString(err) + " " + ErrorText(err));
      if(!IsRetryableError(err) && err != 130) return;
      Sleep(RetryWaitMs);
     }
   Log("警告: 損切り・利確を設定できませんでした。手動で確認してください。");
  }

//+------------------------------------------------------------------+
//| 自分のEAのポジション数を数える                                     |
//+------------------------------------------------------------------+
int CountMyPositions()
  {
   int count = 0;
   for(int i = 0; i < OrdersTotal(); i++)
     {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber) continue;
      if(OrderType() == OP_BUY || OrderType() == OP_SELL) count++;
     }
   return(count);
  }

//+------------------------------------------------------------------+
//| 指定した向きのポジションを決済する(失敗したら再試行する)         |
//+------------------------------------------------------------------+
void CloseMyPositions(int type)
  {
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
      if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber) continue;
      if(OrderType() != type) continue;

      int ticket = OrderTicket();
      double lots = OrderLots();

      for(int attempt = 1; attempt <= MaxRetry; attempt++)
        {
         if(!RefreshRates()) { Sleep(RetryWaitMs); continue; }
         double closePrice = (type == OP_BUY) ? Bid : Ask;

         if(OrderClose(ticket, lots, closePrice, g_slippage, clrGray))
           {
            Log("決済成功: チケット " + IntegerToString(ticket) + "(反対側の水準に到達)");
            break;
           }

         int err = GetLastError();
         Log("決済失敗(" + IntegerToString(attempt) + "回目): チケット " +
             IntegerToString(ticket) + " エラー " + IntegerToString(err) + " " + ErrorText(err));

         if(!IsRetryableError(err)) break;
         Sleep(RetryWaitMs);
        }
     }
  }

//+------------------------------------------------------------------+
//| よく出るエラーを日本語にする                                       |
//+------------------------------------------------------------------+
string ErrorText(int err)
  {
   switch(err)
     {
      case 1:   return("(変更なし)");
      case 4:   return("(サーバーが混雑)");
      case 6:   return("(接続なし)");
      case 128: return("(取引のタイムアウト)");
      case 129: return("(価格が正しくない)");
      case 130: return("(損切り・利確が近すぎる)");
      case 131: return("(ロット数が不正)");
      case 132: return("(市場が閉じている)");
      case 133: return("(取引が禁止されている)");
      case 134: return("(証拠金不足)");
      case 135: return("(価格が変わった)");
      case 136: return("(気配値が出ていない)");
      case 138: return("(リクオート)");
      case 145: return("(決済に近すぎて変更できない)");
      case 146: return("(取引処理が混雑)");
      case 4108: return("(チケット番号が不正)");
      case 4109: return("(自動売買が許可されていない)");
     }
   return("");
  }
//+------------------------------------------------------------------+

貼り付けたあと、1行目が //+--- で始まり、最後の行も //+--- で終わっていれば、全文が正しく入っています。

手順7 保存して、保存場所を確認する

[Ctrl]+[S] で保存します。「ファイル」→「保存」でも同じです。タブのファイル名の横に「*」が付いていたら未保存のしるしなので、消えたことを確認してください。要確認

EAのファイルは、必ず次の場所に保存されます。

  • MT4のデータフォルダ → MQL4Experts

ウィザードから作ったファイルは自動でここに入るので、保存場所を自分で選ぶ必要はありません。

場所を確認したいときは、MT4の「ファイル」→「データフォルダを開く」を押すと、エクスプローラーでデータフォルダが開きます。要確認 その中の MQL4\ExpertsRSI_Reversal_Sample.mq4 があれば正解です。

MetaEditor左側のナビゲータでも、「Experts」フォルダの下に「RSI_Reversal_Sample.mq4」が表示されます。

つまずきポイント

  • MetaEditorが開かない: MT4が起動しているか確認します。[F4] が効かないときは、MT4のツールバーのアイコンか、Windowsのスタートメニューの「MetaEditor」から開きます。要確認
  • ウィザードに「エキスパートアドバイザ」が2種類ある: 「(テンプレート)」と付いているほうを選びます。「(生成)」のほうは質問形式でコードを作る機能で、今回は使いません。要確認
  • 貼り付けたら日本語が文字化けした: コメント(// の行)の日本語が化けているだけなら、動作には影響しません。気になる場合は、MetaEditorの「ファイル」メニューからエンコードを UTF-8 にして保存し直します。要確認
  • 貼り付けが途中までしか入っていない: コピー元の範囲選択が足りていません。コードブロックの「コピー」ボタンを使ってやり直してください。
  • デスクトップなど別の場所に保存してしまった: MT4は MQL4\Experts の中しか見ません。エクスプローラーでファイルを MQL4\Experts に移動してください。

次の記事

次は、貼り付けたコードを「コンパイル」して、MT4が実行できる形(.ex4)に変換します。