この記事でできるようになること
- 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のデータフォルダ →
MQL4→Experts
ウィザードから作ったファイルは自動でここに入るので、保存場所を自分で選ぶ必要はありません。
場所を確認したいときは、MT4の「ファイル」→「データフォルダを開く」を押すと、エクスプローラーでデータフォルダが開きます。要確認 その中の MQL4\Experts に RSI_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)に変換します。