MENU

【PO-EA#1】AIでEA作成は可能?Claudeのコードをエンジニアが修正した驚きの結果

AIでEA作成は可能?Claudeのコードをエンジニアが修正した驚きの結果

「最近話題のAIを使えば、プログラムがわからなくても、勝てる自動売買(EA)が作れるかも」…そんな風に考えたことはありませんか?

AIでEAを作ろう、ロジックを作ろうと試したことがある方は少なくないと思います。

今回は、高性能AIのClaudeに、王道のロジック「パーフェクトオーダーでエントリーするEA」を作成してもらいました。プログラミングの知識がなくても、指示書(プロンプト)さえあればコードが一瞬で出来上がるので便利ですが、結論から言うと、AIはコードを書く能力は抜群ですが、相場のお作法までは理解していませんでした。

AIが作成したコードをそのままバックテストしたところ、14ヶ月で403回という取引数を記録。一見活発に見えますが、中身はレンジ相場での『地獄の往復ビンタ』による暴走状態でした。

そこで、現役エンジニアの視点でこのガバガバなロジックを大改造し、取引を42回まで厳選。AIの『素案』を『実戦レベルの検証用コード』へ昇華させた記録をまとめました。

目次

AI(Claude)に投げた指示書(プロンプト)

まずは、AIに伝えた指示書をそのまま記載します。

事前に、AI(Gemini)にパーフェクトオーダーでエントリーするEAのロジックを考えてもらい、指示書も作ってもらいました。

指示書

依頼内容:MT4(MQL4)用のEA作成
ロジック:

  1. 移動平均線(EMA)3本(20, 75, 200)のパーフェクトオーダーでエントリー。
  2. AIフィルター条件(追加):長期EMA(200)の角度が一定以上であること(直近5本の変化量)。
    ATR(期間14)が過去50本の平均より高いこと(ボラティリティがある時のみ)。
  1. 決済: 短期EMAと中期EMAがデッドクロス(買いの場合)したらクローズ。

仕様:

  • 国内ブローカー向けに、マジックナンバー、ロット数、スリッページ、スプレッド制限をパラメーターで調整可能にしてください。
  • 日本語でコメントを入れてください。

よくあるパーフェクトオーダーのエントリー条件に、MAの傾きをフィルターとして追加したものになります。

決済は短期EMAと中期EMAが逆方向にクロスしたら、という条件です。

日本語でコメントを入れる指示は、後でコードを見やすくするためのものですね。

出来上がったコードはこちらです(長いので、アコーディオンにしています)

AIが作成したコード
//+------------------------------------------------------------------+
//|                                           PerfectOrder_AI_EA.mq4 |
//|                               Copyright 2026, MQL Lab Tomoshibi. |
//|                                    https://mql-lab-tomoshibi.com |
//+------------------------------------------------------------------+
#property copyright "Perfect Order EA with AI Filter"
#property copyright "Copyright 2026, MQL Lab Tomoshibi."
#property link "https://mql-lab-tomoshibi.com"
#property version   "1.00"
#property strict

//--- 入力パラメーター
input int      MagicNumber = 123456;           // マジックナンバー
input double   LotSize = 0.1;                  // ロットサイズ
input int      Slippage = 3;                   // スリッページ(pips)
input int      MaxSpread = 30;                 // 最大スプレッド(ポイント)

//--- EMAパラメーター
input int      EMA_Short = 20;                 // 短期EMA期間
input int      EMA_Medium = 75;                // 中期EMA期間
input int      EMA_Long = 200;                 // 長期EMA期間

//--- AIフィルターパラメーター
input double   MinEMA200Angle = 0.0001;        // EMA200の最小角度(変化量)
input int      EMA200AngleBars = 5;            // EMA200角度計算本数
input int      ATR_Period = 14;                // ATR期間
input int      ATR_CheckBars = 50;             // ATR比較本数
input double   ATR_Multiplier = 1.0;           // ATR倍率(平均の何倍以上か)

//--- グローバル変数
int ticket = -1;
bool tradeAllowed = true;

//+------------------------------------------------------------------+
//| Expert initialization function                                     |
//+------------------------------------------------------------------+
int OnInit()
{
   Print("===== パーフェクトオーダー AI EA 起動 =====");
   Print("短期EMA: ", EMA_Short, " / 中期EMA: ", EMA_Medium, " / 長期EMA: ", EMA_Long);
   Print("マジックナンバー: ", MagicNumber);
   Print("ロットサイズ: ", LotSize);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Print("===== EA 停止 =====");
}

//+------------------------------------------------------------------+
//| Expert tick function                                               |
//+------------------------------------------------------------------+
void OnTick()
{
   // スプレッドチェック
   double spread = MarketInfo(Symbol(), MODE_SPREAD);
   if(spread > MaxSpread)
   {
      return; // スプレッドが広すぎる場合は取引しない
   }
   
   // EMA値を取得(現在の足)
   double ema_short_current = iMA(Symbol(), 0, EMA_Short, 0, MODE_EMA, PRICE_CLOSE, 0);
   double ema_medium_current = iMA(Symbol(), 0, EMA_Medium, 0, MODE_EMA, PRICE_CLOSE, 0);
   double ema_long_current = iMA(Symbol(), 0, EMA_Long, 0, MODE_EMA, PRICE_CLOSE, 0);
   
   // EMA値を取得(1本前の足)- デッドクロス判定用
   double ema_short_prev = iMA(Symbol(), 0, EMA_Short, 0, MODE_EMA, PRICE_CLOSE, 1);
   double ema_medium_prev = iMA(Symbol(), 0, EMA_Medium, 0, MODE_EMA, PRICE_CLOSE, 1);
   
   // AIフィルター条件をチェック
   bool aiFilterPassed = CheckAIFilter();
   
   // ポジション保有チェック
   bool hasPosition = false;
   int positionType = -1;
   
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
         {
            hasPosition = true;
            positionType = OrderType();
            ticket = OrderTicket();
            break;
         }
      }
   }
   
   // ポジションがある場合は決済条件をチェック
   if(hasPosition)
   {
      CheckCloseCondition(positionType, ema_short_current, ema_medium_current, 
                         ema_short_prev, ema_medium_prev);
   }
   else
   {
      // ポジションがない場合はエントリー条件をチェック
      if(aiFilterPassed)
      {
         // 買いパーフェクトオーダー(上昇トレンド)
         if(ema_short_current > ema_medium_current && 
            ema_medium_current > ema_long_current)
         {
            OpenBuyOrder();
         }
         // 売りパーフェクトオーダー(下降トレンド)
         else if(ema_short_current < ema_medium_current && 
                 ema_medium_current < ema_long_current)
         {
            OpenSellOrder();
         }
      }
   }
}

//+------------------------------------------------------------------+
//| AIフィルター条件チェック                                              |
//+------------------------------------------------------------------+
bool CheckAIFilter()
{
   // 1. EMA200の角度チェック
   double ema200_current = iMA(Symbol(), 0, EMA_Long, 0, MODE_EMA, PRICE_CLOSE, 0);
   double ema200_past = iMA(Symbol(), 0, EMA_Long, 0, MODE_EMA, PRICE_CLOSE, EMA200AngleBars);
   double ema200_change = MathAbs(ema200_current - ema200_past);
   
   if(ema200_change < MinEMA200Angle)
   {
      return false; // 角度が不十分
   }
   
   // 2. ATRボラティリティチェック
   double atr_current = iATR(Symbol(), 0, ATR_Period, 0);
   
   // 過去50本のATR平均を計算
   double atr_sum = 0;
   for(int i = 1; i <= ATR_CheckBars; i++)
   {
      atr_sum += iATR(Symbol(), 0, ATR_Period, i);
   }
   double atr_average = atr_sum / ATR_CheckBars;
   
   if(atr_current < atr_average * ATR_Multiplier)
   {
      return false; // ボラティリティが不十分
   }
   
   return true; // すべてのAIフィルター条件をクリア
}

//+------------------------------------------------------------------+
//| 買いポジションオープン                                                |
//+------------------------------------------------------------------+
void OpenBuyOrder()
{
   double ask = MarketInfo(Symbol(), MODE_ASK);
   int slippage_points = Slippage * 10; // pipsをポイントに変換
   
   ticket = OrderSend(Symbol(), OP_BUY, LotSize, ask, slippage_points, 
                     0, 0, "PerfectOrder Buy", MagicNumber, 0, clrBlue);
   
   if(ticket > 0)
   {
      Print("買いポジションオープン成功: チケット#", ticket, " 価格:", ask);
   }
   else
   {
      Print("買いポジションオープン失敗: エラー", GetLastError());
   }
}

//+------------------------------------------------------------------+
//| 売りポジションオープン                                                |
//+------------------------------------------------------------------+
void OpenSellOrder()
{
   double bid = MarketInfo(Symbol(), MODE_BID);
   int slippage_points = Slippage * 10; // pipsをポイントに変換
   
   ticket = OrderSend(Symbol(), OP_SELL, LotSize, bid, slippage_points, 
                     0, 0, "PerfectOrder Sell", MagicNumber, 0, clrRed);
   
   if(ticket > 0)
   {
      Print("売りポジションオープン成功: チケット#", ticket, " 価格:", bid);
   }
   else
   {
      Print("売りポジションオープン失敗: エラー", GetLastError());
   }
}

//+------------------------------------------------------------------+
//| 決済条件チェック                                                     |
//+------------------------------------------------------------------+
void CheckCloseCondition(int posType, double ema_s_curr, double ema_m_curr,
                        double ema_s_prev, double ema_m_prev)
{
   bool shouldClose = false;
   
   if(posType == OP_BUY)
   {
      // 買いポジション:短期EMAが中期EMAを下抜け(デッドクロス)
      if(ema_s_prev >= ema_m_prev && ema_s_curr < ema_m_curr)
      {
         shouldClose = true;
         Print("買いポジション決済シグナル: デッドクロス検出");
      }
   }
   else if(posType == OP_SELL)
   {
      // 売りポジション:短期EMAが中期EMAを上抜け(ゴールデンクロス)
      if(ema_s_prev <= ema_m_prev && ema_s_curr > ema_m_curr)
      {
         shouldClose = true;
         Print("売りポジション決済シグナル: ゴールデンクロス検出");
      }
   }
   
   if(shouldClose)
   {
      ClosePosition(posType);
   }
}

//+------------------------------------------------------------------+
//| ポジションクローズ                                                   |
//+------------------------------------------------------------------+
void ClosePosition(int posType)
{
   if(OrderSelect(ticket, SELECT_BY_TICKET))
   {
      double closePrice;
      color closeColor;
      
      if(posType == OP_BUY)
      {
         closePrice = MarketInfo(Symbol(), MODE_BID);
         closeColor = clrBlue;
      }
      else
      {
         closePrice = MarketInfo(Symbol(), MODE_ASK);
         closeColor = clrRed;
      }
      
      int slippage_points = Slippage * 10;
      bool result = OrderClose(ticket, OrderLots(), closePrice, slippage_points, closeColor);
      
      if(result)
      {
         Print("ポジションクローズ成功: チケット#", ticket, " 価格:", closePrice);
         ticket = -1;
      }
      else
      {
         Print("ポジションクローズ失敗: エラー", GetLastError());
      }
   }
}
//+------------------------------------------------------------------+

コンパイルもできましたし、それっぽいものが出来上がっていますね。

このEAが想定通りに動くならば、すごくお手軽なのですが…?

戦慄のバックテスト結果

とりあえず、まずはバックテストをしてみました。

今回は「AIが作成したEAがどの程度動くのか?」がメインの検証なので、パラメータ設定はデフォルトのままで動かします。

設定は以下の通り。

  • 通貨ペア:USDJPY
  • 時間足:1時間足
  • スプレッド:20point(2.0pips)
  • 期間:2021年1月1日~2025年12月31日(5年間)
  • 初期資金:10万円

ちなみに、私はTick Data Suiteを持っているので、そちらで行います。

Tick Data Suiteは、有料ではあるのですが、MT4で制度の高いヒストリカルデータをバックテストに使用できるツールです。

品質の良いヒストリカルデータを使える以外にも様々な機能があるのですが使いこなせていないので、いずれは使いこなしたいところです。

さて、バックテストをした結果は以下です。

AIが作成したEAのバックテスト結果

グラフが何とも歪な右肩下がりですが、2点ほど注目すべきところがありますね。

  • 取引回数が403回
  • 最大ドローダウンが65.29%(金額では83,052円)

実はこのバックテスト、最後の取引が「2022.02.28 22:00」なので、ほぼ14ヶ月しか稼働できていません。

バックテストの最後の取引時刻

14ヶ月で403回の取引となると、1ヶ月に28回。パーフェクトオーダー条件での取引と考えると、多すぎるのです。

また、最大ドローダウンが約65%というのも考えものです。王道のロジックの割に、リスクが高い結果になっていますね。

原因は、何だと思いますか?

原因は「エントリータイミング」にあった

結論から言うと「エントリータイミング」がAIは理解できていなかったんですね。

皆さんは「移動平均線(EMA)3本(20, 75, 200)のパーフェクトオーダーでエントリー。」と言うと、何となく「パーフェクトオーダーが始まった足でエントリーなんだな」と思いますよね?

でも、AIは理解できないんです。

作成されたコードは、AIフィルターを除くと以下のような条件でエントリーしていました。

  • ポジションを保有していない状態で、最新足で20EMA>75EMA>200EMA(上昇パーフェクトオーダー)であれば買いエントリー(売りは逆)

最新足では、値動きに合わせて各EMAは上下に動きます。

つまり、ちょうど20EMAと75EMAが触れたり離れたりするあたりで値動きが前後すると、20EMAと75EMAのデッドクロスで買いポジション決済、20EMAと75EMAが離れて上昇パーフェクトオーダーになったので買いエントリー、また20EMAと75EMAのデッドクロスで買いポジション決済…という地獄の「エントリーと決済の繰り返し」が起こります。

また、上記は「上昇(下降)パーフェクトオーダーの始まり」という条件はありませんので、上昇パーフェクトオーダーの終盤の方でも、ポジションを持っていなければエントリーしてしまうのです。

というわけで、以下を修正してみました。

  • エントリータイミングを「最新足確定時」のみに限定
  • パーフェクトオーダー条件は「1本前はパーフェクトオーダー条件を満たしていない」という条件を追加
  • ロジック条件や発注処理のレートに「NormalizeDouble」関数を追加
  • 決済時のスリッページ制限を実質無効化
  • 決済時のスプレッドフィルターを無効化
  • MA種別をパラメータ設定で変更できるように(3本共通の設定)

決済時のスリッページ制限やスプレッドの無効化は、決済は決済条件を満たしたら行ってほしいからです。

確かに、売りポジションは決済時のスプレッドが反映されますが、だからといって決済しない選択肢はないのかなと思いますよね。

エントリータイミングは、先程の「地獄のエントリーと決済の繰り返し」を回避できます。

また、バックテストは所詮疑似的に作られたティックなので、最新足の値動きに合わせたエントリー条件はあまりバックテスト結果が参考にならない事が多いですから、できれば最新足確定時のエントリーにすると、検証しやすいかもしれませんね。

「NormalizeDouble」関数は、MT4やMT5でのお作法のようなものです。

ここに触れると少しややこしいのでまた別の機会に詳しく解説させていただきますが、簡単に言うとEAの小さな誤作動をなるべく防ぎたい場合に必要な関数ですね。

今回は使っていませんが、検証に便利なのでMA種別もパラメータ設定で変えられるようにしておきました。

コードを全て載せると長いので一部だけですが、エントリー条件はこんな感じになってます。

void OnTick()
  {
   // 最新足データがない場合は最新足をデータに入れる
   if(NewBarTime==0)
      NewBarTime = Time[0];
   
   // 足確定時のみ判定
   if(NewBarTime==Time[0])
      return;
   
   NewBarTime = Time[0];
   
   (中略)
   
   if(!hasPosition)
     {
      // ポジションがない場合はエントリー条件をチェック
      if(aiFilterPassed)
        {
         // 買いパーフェクトオーダー(上昇トレンド)
         if(NormalizeDouble(ma_short_current, _Digits+2)>NormalizeDouble(ma_medium_current, _Digits+2) &&
            NormalizeDouble(ma_medium_current, _Digits+2)>NormalizeDouble(ma_long_current, _Digits+2) &&
            !(NormalizeDouble(ma_short_prev, _Digits+2)>NormalizeDouble(ma_medium_prev, _Digits+2) &&
              NormalizeDouble(ma_medium_prev, _Digits+2)>NormalizeDouble(ma_long_prev, _Digits+2)))
           {
            PrintFormat("[%d] 買いエントリー", __LINE__);
            OpenBuyOrder();
           }
         // 売りパーフェクトオーダー(下降トレンド)
         else
         if(NormalizeDouble(ma_short_current, _Digits+2)<NormalizeDouble(ma_medium_current, _Digits+2) &&
            NormalizeDouble(ma_medium_current, _Digits+2)<NormalizeDouble(ma_long_current, _Digits+2) &&
            !(NormalizeDouble(ma_short_prev, _Digits+2)<NormalizeDouble(ma_medium_prev, _Digits+2) &&
              NormalizeDouble(ma_medium_prev, _Digits+2)<NormalizeDouble(ma_long_prev, _Digits+2)))
           {
            PrintFormat("[%d] 売りエントリー", __LINE__);
            OpenSellOrder();
           }
        }
     }
  }

修正後のバックテスト結果

修正後のバックテスト結果はこうです。

修正後のバックテスト結果

負けてる

まあ、エントリータイミングやパーフェクトオーダー条件の追加だけで、劇的に勝てるようになるわけではないので想定内ですね。

注目すべきところは、先程の2点の変化です。

  • 取引回数が403回⇒42回
  • 最大ドローダウンが65.29%(金額では83,052円)⇒62.08%(金額では68908.55円)

取引回数が、だいたい9分の1になりました。

1時間足チャートでパーフェクトオーダー条件でのエントリーと考えれば、まあ妥当でしょうか。

ドローダウンについては、ほぼ負けっぱなしなので致し方ないですね…

ちなみに、AIが作った初期型よりはほんの少し生き延びています。

修正後のバックテストの最後の取引時刻

ほぼ誤差

何なら、プロフィットファクターは修正前の方がよいという…

とはいえ、設定を変えてバックテストの検証をしたり、TPやSLで利益確定や損失確定をしていないので、改良の余地ありといったところでしょうか。

ちなみに、今回修正した条件部分については、AIに指示書で正確に伝えることで、自分で修正しなくても良くなると思います。

例えば「エントリータイミングは最新足確定時」とか「1本前は上昇パーフェクトオーダーを満たしていないこと」とかでしょうか。

まとめ:AIは「助手」であって「設計者」ではない

バックテスト結果は何とも言えない結果のままになりましたが…

今回の記事は、あくまで「AIが作成したEAがどの程度動くのか?」の検証なので、それはそれで良しとしましょう。

AIはとても便利ですが、指示が曖昧だと今回のように「思ったのと違う」動作をするEAが出来上がります。

また、ロジックの特徴を把握していないと、バックテストしたときに「これが正しい動作なのか」がわかりません。

AIに頼りっきりになってしまうと、再現性のないEAでパラメータ設定を最適化し「なんか勝てない…」となってしまうわけですね。

あくまでAIは「助手」として、最終的には自分でチェックや追加指示をするという使い方が良さそうです。

次回予告

次回は、せっかく作ったこの王道EAを、右肩上がりにするためにカスタマイズしていきます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

開発エンジニア。「AIでEAを作ったが動かない」という壁を突破するための、修正のコツや検証技術を発信しています。作成代行や販売経験を活かし、皆さんが自分のロジックを自由に形にできるための小さな「燈」を灯します。 (※現在は作成代行の受付は行っておりません)

コメント

コメントする

CAPTCHA


目次