スキャルピングEAの作り方|ロジック・ソースコード公開と高勝率EAの検証

スキャルピングEAの作り方とMQL4ソースコード公開・バックテスト検証
EAサンプル・ノウハウ
記事内に商品プロモーションを含む場合があります
スポンサーリンク

「高勝率のスキャルピングEAは、どうやって作ればいいのか?」
この記事では、実際にMT4用のスキャルピングEAを設計し、バックテストしながら改善した流れを、初級~中級者向けにできるだけわかりやすく紹介します。

今回作成したのは、USDJPYの5分足を対象にしたAIスキャルピングEA V1です。
EMAで大きな方向を確認し、ストキャスティクスで押し目・戻りを探し、ATRで値動きが極端な場面を避けるシンプルな構成にしています。

今回の最終結果

2025年通年・USDJPY M5・全ティック・スプレッド5pointsで検証した結果、
698取引 / 勝率85.39% / PF1.18 / 最大DD2.89%となりました。

ナンピン・マーチンは使わず、1回の取引リスクは口座残高の0.5%を基準にしています。

スキャルピングEAの作り方|まずロジックを決める

EAを作るときは、いきなりMQL4のコードを書くよりも、まず「どんな場面で買う・売るのか」を整理した方が作りやすくなります。

今回のEAでは、単純にストキャスティクスが売られすぎ・買われすぎになっただけで逆張りするのではなく、大きなトレンド方向に対する押し目・戻りを狙う形にしました。

役割使用する指標今回の設定
トレンド方向を確認EMAEMA50 / EMA200
押し目・戻りを探すStochastic5,3,3 / Buy 17 / Sell 90
値動きの大きさを確認ATRATR14 / 2.5~12pips
利確・損切り固定TP / SLTP 5pips / SL 25pips
資金管理残高連動ロット1回の損失リスク0.5%

買いエントリーの条件

買いは、EMA50がEMA200より上にある上昇方向で、ストキャスティクスがかなり低い位置まで下がったあとに上向きへクロスした場面を狙います。

BUYの考え方
EMA50 > EMA200
ストキャスティクスのKが17未満
前の足ではKがD以下、直近確定足でKがDを上抜け
ATRが2.5~12pipsの範囲内

上昇トレンド中でも、価格が一時的に下がる場面はあります。そこでストキャスティクスを使い、「上昇方向の中で、かなり深く押したところ」だけを拾うイメージです。

売りエントリーの条件

売りは反対で、EMA50がEMA200より下にある下降方向で、ストキャスティクスが高い位置まで戻したあとに下向きへクロスした場面を狙います。

SELLの考え方
EMA50 < EMA200
ストキャスティクスのKが90超
前の足ではKがD以上、直近確定足でKがDを下抜け
ATRが2.5~12pipsの範囲内

買い側が17、売り側が90と左右対称ではありません。これは後述するバックテストで、売り側はかなり深く引きつけた方が成績が安定したためです。

決済はTP5pips・SL25pips

利確は5pips、損切りは25pipsです。数字だけを見ると損大利小ですが、高勝率型のスキャルピングEAではこのような設計になることがあります。

ただし、利確幅を小さくして損切りを大きくするだけでは勝てません。今回の検証でも、SLだけを変更した段階ではPF1を超えませんでした。

PF(プロフィットファクタ)とは?
バックテストの「総利益 ÷ 総損失」で計算される指標です。
PF1.00を超えるとテスト上は利益が損失を上回り、数値が高いほど利益側が大きいことを示します。

2025年通年の最終バックテスト結果

まずは、完成したV1の2025年通年バックテスト結果を先に確認します。

項目結果
通貨ペアUSDJPY
時間足M5(5分足)
期間2025年1月~12月
モデル全ティック
スプレッド5points
初期証拠金1,000,000円
1回のリスク口座残高の0.5%
総取引数698回
勝率85.39%
PF1.18
純益+92,113.30円
最大DD2.89%
AIスキャルピングEA V1の2025年USDJPY M5バックテスト結果
AIスキャルピングEA V1|2025年通年・USDJPY M5・スプレッド5points

勝率は85%を超えましたが、PFは1.18です。「高勝率だから非常に強いEA」という見方ではなく、勝率と損益の両方を見ることが重要です。

また、最大DDは2.89%でした。今回は1回のSL到達時に口座残高のおよそ0.5%を失うようロットを自動計算しています。

AIスキャルピングEA V1の2025年損益曲線
序盤に停滞・下落する期間もありますが、2025年全体では右肩上がりとなりました。

ここからは、この結果になるまでにどこを見直したのかを、要点だけ紹介します。

最初から勝てたわけではない|バックテストしながら改善

今回のEAも、最初からPF1.18になったわけではありません。初期版は勝率こそ高かったものの、PF0.87でトータルはマイナスでした。

そこで、まずSLを15~30pipsまで変えて確認しましたが、PFは0.86~0.91の範囲で、どの設定でもPF1を超えませんでした。
この結果から、損切り幅ではなくエントリー条件そのものを見直す必要があると判断しました。

見直したポイント結果判断
初期ロジックPF0.87高勝率でもマイナス
SLを15~30pipsで比較PF0.86~0.91SL調整だけでは改善せず
エントリー位置を厳選開発期間PF1.20Buy17 / Sell90を採用

特に効果があったのは、ストキャスティクスでより深い押し目・戻りまで待つようにしたことです。最終的に開発期間ではBuyLevel=17 / SellLevel=90でPF1.20まで改善しました。

逆に、EMA50の傾きまで条件に追加すると取引数が減り、PFも悪化したため不採用にしています。条件を増やせば成績が良くなるわけではないという点も、EA作りでは重要です。

EA改善のポイント
数字が悪いときにパラメータを闇雲に増やすのではなく、どの条件が結果に影響しているのかを一つずつ確認します。
今回の場合は、SL調整よりもエントリーを厳選する方が効果的でした。

調整に使っていない期間でも確認する

バックテストで注意したいのが、調整した期間だけ良く見えるEAを作ってしまうことです。

そこで今回は2025年をすべて調整に使わず、1~9月をロジック調整用、10~12月を確認用に分けました。

OOS(Out of Sample)とは?
パラメータ調整やロジック改善に使っていない「未使用期間」で成績を確認する方法です。
本記事では2025年10~12月をOOS期間とし、この期間の結果を確認したあとに成績を良くするための再調整は行っていません。
期間用途取引数勝率PF
2025年1~9月ロジック調整54285.61%1.20
2025年10~12月未使用期間で確認15684.62%1.11

未使用期間ではPFが1.20から1.11へ下がりましたが、勝率は84.62%を維持し、トータルでもプラスでした。

ここでPF1.20へ戻すために10~12月の結果を見ながら再調整すると、せっかく分けた未使用期間まで調整データになってしまいます。そのため、追加最適化は行わず、そのままV1の最終仕様としました。

スプレッドが広がった場合も確認

スキャルピングEAは利確幅が小さいため、スプレッドの影響を受けやすいです。

そこで、ロジックを変えずにスプレッドだけ3・5・10pointsへ変更して確認しました。

スプレッド取引数勝率PF
3points70085.43%1.18
5points69885.39%1.18
10points69283.67%1.04

3~5pointsではほぼ同じ成績でしたが、10pointsになるとPFは1.04まで低下しました。

TPが5pipsしかないEAなので、スプレッドが広がるほど不利になります。今回のV1では最大スプレッド5pointsを初期設定としています。

AIスキャルピングEA V1のMQL4ソースコード

以下が今回作成したV1のソースコードです。MT4用のMQL4コードで、EMA・ストキャスティクス・ATR、スプレッド制限、リスクベースのロット計算まで含めています。

利用前の注意
このEAはバックテスト結果を紹介する学習・検証用サンプルです。将来の利益を保証するものではありません。
利用する場合は、まずデモ口座やバックテストで動作を確認し、FX業者ごとの最小注文距離・スプレッド・取引条件も確認してください。

EAファイルをダウンロード

用途に合わせてEX4・MQ4の2種類を用意しています。
まず動かしてみたい方はEX4、ソースを確認・編集したい方はMQ4をご利用ください。

EX4版 初心者向け コンパイル不要。MT4へ入れてすぐ試せます。
MQ4版 ソースコードの確認・編集・学習用です。

※以下のソースコードは、上でダウンロードできるMQ4版と同一内容です。EX4版は、このMQ4をコンパイルした実行ファイルです。

//+------------------------------------------------------------------+
//| AI_HighWin_Scalper_v1.00.mq4                                    |
//| 2026 High-Win-Rate Scalping EA - Final Verified Version                 |
//| https://fx-prog.com/                                             |
//+------------------------------------------------------------------+
#property strict
#property version   "1.00"
#property description "USDJPY M5 high-win-rate scalper - verified 2025 backtest version"
#property description "EMA trend + Stochastic pullback + ATR volatility filter"
#property description "No martingale / no averaging down / one position per symbol+magic"

//--- Verified baseline
// USDJPY / M5 / 2025 full year / Every tick / spread 5 points
// EMA50/EMA200 trend filter
// Stochastic(5,3,3): BuyLevel=17 / SellLevel=90
// ATR(14): 2.5 - 12.0 pips
// TP=5 pips / SL=25 pips / Risk=0.5% of balance
// No martingale / no averaging down / one position per symbol+magic
//
// 2025 reference result (FXTF-Demo):
// 698 trades / win rate 85.39% / PF 1.18 / max DD 2.89%
// OOS 2025-10-01 to 2025-12-31: win rate 84.62% / PF 1.11
// Backtest results do not guarantee future performance.

//--- Signal settings
input ENUM_TIMEFRAMES SignalTimeframe = PERIOD_M5;

//--- Trend filter
input int FastEMAPeriod = 50;
input int SlowEMAPeriod = 200;

//--- Pullback / reversal timing
input int StochKPeriod  = 5;
input int StochDPeriod  = 3;
input int StochSlowing  = 3;
input int BuyLevel      = 17;
input int SellLevel     = 90;

//--- Volatility filter
input int    ATRPeriod  = 14;
input double ATRMinPips = 2.5;
input double ATRMaxPips = 12.0;

//--- Exit settings
input double TakeProfitPips = 5.0;
input double StopLossPips   = 25.0;

//--- Execution / risk settings
input int    MaxSpreadPoints = 5;
input double BrokerMinDistancePips = 5.0; // FXTF: minimum TP/SL distance
input double RiskPercent     = 0.5;
input int    SlippagePoints  = 20;
input int    MagicNumber     = 26082410;

//--- Internal state
datetime g_lastBarTime = 0;

//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   if(FastEMAPeriod <= 0 ||
      SlowEMAPeriod <= FastEMAPeriod ||
      StochKPeriod <= 0 ||
      StochDPeriod <= 0 ||
      StochSlowing <= 0 ||
      BuyLevel < 0 || BuyLevel > 100 ||
      SellLevel < 0 || SellLevel > 100 ||
      BuyLevel >= SellLevel ||
      ATRPeriod <= 0 ||
      ATRMinPips < 0.0 ||
      ATRMaxPips <= ATRMinPips ||
      TakeProfitPips <= 0.0 ||
      StopLossPips <= 0.0 ||
      MaxSpreadPoints < 0 ||
      BrokerMinDistancePips < 0.0 ||
      TakeProfitPips + 1e-8 < BrokerMinDistancePips ||
      StopLossPips + 1e-8 < BrokerMinDistancePips ||
      RiskPercent <= 0.0)
   {
      Print("Invalid input parameters.");
      return(INIT_PARAMETERS_INCORRECT);
   }

   g_lastBarTime = iTime(Symbol(), SignalTimeframe, 0);

   double brokerStopLevelPips =
      (MarketInfo(Symbol(), MODE_STOPLEVEL) * Point) / PipSize();

   Print("AI_HighWin_Scalper_v1.00 initialized. Symbol=", Symbol(),
         " TF=", SignalTimeframe,
         " Digits=", Digits,
         " Point=", DoubleToString(Point, Digits),
         " PipSize=", DoubleToString(PipSize(), Digits),
         " TP=", DoubleToString(TakeProfitPips, 1), " pips",
         " SL=", DoubleToString(StopLossPips, 1), " pips",
         " UserMinDistance=", DoubleToString(BrokerMinDistancePips, 1), " pips",
         " BrokerStopLevel=", DoubleToString(brokerStopLevelPips, 1), " pips",
         " MaxSpread=", MaxSpreadPoints, " points",
         " Risk=", DoubleToString(RiskPercent, 2), "%");

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick                                                      |
//+------------------------------------------------------------------+
void OnTick()
{
   if(!IsNewSignalBar())
      return;

   if(!EnoughBars())
      return;

   // One market position per symbol + MagicNumber
   if(HasOpenPosition())
      return;

   if(!SpreadIsAcceptable())
      return;

   int signal = GetEntrySignal(); // 1=BUY, -1=SELL, 0=NONE
   if(signal == 0)
      return;

   OpenPosition(signal);
}

//+------------------------------------------------------------------+
//| Detect a new bar on the signal timeframe                         |
//+------------------------------------------------------------------+
bool IsNewSignalBar()
{
   datetime currentBarTime = iTime(Symbol(), SignalTimeframe, 0);

   if(currentBarTime <= 0)
      return(false);

   if(currentBarTime == g_lastBarTime)
      return(false);

   g_lastBarTime = currentBarTime;
   return(true);
}

//+------------------------------------------------------------------+
//| Ensure enough history exists                                     |
//+------------------------------------------------------------------+
bool EnoughBars()
{
   int required = SlowEMAPeriod + 20;
   int bars = iBars(Symbol(), SignalTimeframe);

   if(bars < required)
   {
      Print("Not enough bars. Required=", required, " Current=", bars);
      return(false);
   }

   return(true);
}

//+------------------------------------------------------------------+
//| Spread filter                                                    |
//| MaxSpreadPoints uses MT4 points.                                 |
//| Example on 3/5-digit FX: 5 points = 0.5 pip.                     |
//+------------------------------------------------------------------+
bool SpreadIsAcceptable()
{
   double spreadPoints = MarketInfo(Symbol(), MODE_SPREAD);

   if(spreadPoints > MaxSpreadPoints)
   {
      Print("Spread filter: current=", DoubleToString(spreadPoints, 0),
            " points, max=", MaxSpreadPoints);
      return(false);
   }

   return(true);
}

//+------------------------------------------------------------------+
//| Entry logic                                                      |
//| Uses only closed bars (shift 1 and 2).                           |
//+------------------------------------------------------------------+
int GetEntrySignal()
{
   //--- Trend filter: EMA50 vs EMA200 on the latest closed bar
   double fastEMA = iMA(Symbol(), SignalTimeframe, FastEMAPeriod, 0,
                        MODE_EMA, PRICE_CLOSE, 1);

   double slowEMA = iMA(Symbol(), SignalTimeframe, SlowEMAPeriod, 0,
                        MODE_EMA, PRICE_CLOSE, 1);

   //--- Stochastic 5,3,3 / SMA / Low-High
   double k1 = iStochastic(Symbol(), SignalTimeframe,
                           StochKPeriod, StochDPeriod, StochSlowing,
                           MODE_SMA, 0, MODE_MAIN, 1);

   double d1 = iStochastic(Symbol(), SignalTimeframe,
                           StochKPeriod, StochDPeriod, StochSlowing,
                           MODE_SMA, 0, MODE_SIGNAL, 1);

   double k2 = iStochastic(Symbol(), SignalTimeframe,
                           StochKPeriod, StochDPeriod, StochSlowing,
                           MODE_SMA, 0, MODE_MAIN, 2);

   double d2 = iStochastic(Symbol(), SignalTimeframe,
                           StochKPeriod, StochDPeriod, StochSlowing,
                           MODE_SMA, 0, MODE_SIGNAL, 2);

   //--- ATR volatility filter
   double atrPrice = iATR(Symbol(), SignalTimeframe, ATRPeriod, 1);
   double atrPips  = atrPrice / PipSize();

   if(atrPips < ATRMinPips || atrPips > ATRMaxPips)
      return(0);

   //--- BUY
   // Uptrend + oversold pullback + bullish Stochastic cross
   bool buySignal =
      (fastEMA > slowEMA) &&
      (k1 < BuyLevel) &&
      (k2 <= d2) &&
      (k1 > d1);

   if(buySignal)
      return(1);

   //--- SELL
   // Downtrend + overbought pullback + bearish Stochastic cross
   bool sellSignal =
      (fastEMA < slowEMA) &&
      (k1 > SellLevel) &&
      (k2 >= d2) &&
      (k1 < d1);

   if(sellSignal)
      return(-1);

   return(0);
}

//+------------------------------------------------------------------+
//| Check open positions for this symbol + MagicNumber               |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;

      if(OrderSymbol() != Symbol())
         continue;

      if(OrderMagicNumber() != MagicNumber)
         continue;

      if(OrderType() == OP_BUY || OrderType() == OP_SELL)
         return(true);
   }

   return(false);
}

//+------------------------------------------------------------------+
//| Open a market position                                           |
//+------------------------------------------------------------------+
bool OpenPosition(int signal)
{
   RefreshRates();

   int orderType;
   double entryPrice;
   double stopLoss;
   double takeProfit;

   double pip = PipSize();

   if(signal > 0)
   {
      orderType  = OP_BUY;
      entryPrice = Ask;
      stopLoss   = entryPrice - StopLossPips * pip;
      takeProfit = entryPrice + TakeProfitPips * pip;
   }
   else
   {
      orderType  = OP_SELL;
      entryPrice = Bid;
      stopLoss   = entryPrice + StopLossPips * pip;
      takeProfit = entryPrice - TakeProfitPips * pip;
   }

   // Minimum TP/SL distance check.
   // FXTF requires at least 5 pips; MODE_STOPLEVEL is also respected.
   double brokerStopLevelPips =
      (MarketInfo(Symbol(), MODE_STOPLEVEL) * Point) / pip;

   double requiredMinPips = MathMax(BrokerMinDistancePips,
                                    brokerStopLevelPips);

   double actualSLPips = MathAbs(entryPrice - stopLoss) / pip;
   double actualTPPips = MathAbs(entryPrice - takeProfit) / pip;

   if(actualSLPips + 1e-8 < requiredMinPips ||
      actualTPPips + 1e-8 < requiredMinPips)
   {
      Print("Order skipped: minimum distance not met.",
            " Required=", DoubleToString(requiredMinPips, 1), " pips",
            " ActualSL=", DoubleToString(actualSLPips, 1), " pips",
            " ActualTP=", DoubleToString(actualTPPips, 1), " pips",
            " MODE_STOPLEVEL=", DoubleToString(brokerStopLevelPips, 1), " pips");
      return(false);
   }

   double lots = CalculateRiskLot(StopLossPips);

   if(lots <= 0.0)
   {
      Print("Order skipped: calculated lot size is invalid.");
      return(false);
   }

   entryPrice = NormalizeDouble(entryPrice, Digits);
   stopLoss   = NormalizeDouble(stopLoss, Digits);
   takeProfit = NormalizeDouble(takeProfit, Digits);

   ResetLastError();

   int ticket = OrderSend(Symbol(),
                          orderType,
                          lots,
                          entryPrice,
                          SlippagePoints,
                          stopLoss,
                          takeProfit,
                          "AI_HighWin_Scalper_v1.00",
                          MagicNumber,
                          0,
                          (orderType == OP_BUY ? clrBlue : clrRed));

   if(ticket < 0)
   {
      int err = GetLastError();
      Print("OrderSend failed. Error=", err,
            " Type=", orderType,
            " Lots=", DoubleToString(lots, 2),
            " Entry=", DoubleToString(entryPrice, Digits),
            " SL=", DoubleToString(stopLoss, Digits),
            " TP=", DoubleToString(takeProfit, Digits));
      return(false);
   }

   Print("Order opened. Ticket=", ticket,
         " Type=", (orderType == OP_BUY ? "BUY" : "SELL"),
         " Lots=", DoubleToString(lots, 2),
         " Entry=", DoubleToString(entryPrice, Digits),
         " SL=", DoubleToString(stopLoss, Digits),
         " TP=", DoubleToString(takeProfit, Digits),
         " SLdist=", DoubleToString(MathAbs(entryPrice-stopLoss)/pip, 1), " pips",
         " TPdist=", DoubleToString(MathAbs(entryPrice-takeProfit)/pip, 1), " pips");

   return(true);
}

//+------------------------------------------------------------------+
//| Risk-based lot calculation                                       |
//| RiskPercent is account-balance based.                            |
//+------------------------------------------------------------------+
double CalculateRiskLot(double stopLossPips)
{
   double riskMoney = AccountBalance() * RiskPercent / 100.0;

   if(riskMoney <= 0.0 || stopLossPips <= 0.0)
      return(0.0);

   double pip = PipSize();
   double stopDistancePrice = stopLossPips * pip;

   // MODE_TICKVALUE = money value of one tick for 1.0 lot.
   // MODE_TICKSIZE  = minimum price movement corresponding to that tick.
   // Therefore: number of ticks to SL = price distance / tick size.
   double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
   double tickSize  = MarketInfo(Symbol(), MODE_TICKSIZE);

   if(tickValue <= 0.0 || tickSize <= 0.0)
   {
      Print("Risk lot error: invalid tick data. TickValue=",
            DoubleToString(tickValue, 8),
            " TickSize=", DoubleToString(tickSize, Digits));
      return(0.0);
   }

   double ticksToSL = stopDistancePrice / tickSize;
   double lossPerLot = ticksToSL * tickValue;

   if(lossPerLot <= 0.0)
      return(0.0);

   double rawLots = riskMoney / lossPerLot;
   double lots = NormalizeLot(rawLots);

   double expectedLoss = lossPerLot * lots;
   double expectedRiskPct = 0.0;
   if(AccountBalance() > 0.0)
      expectedRiskPct = expectedLoss / AccountBalance() * 100.0;

   Print("Risk lot calc: Balance=", DoubleToString(AccountBalance(), 2),
         " RiskMoney=", DoubleToString(riskMoney, 2),
         " SL=", DoubleToString(stopLossPips, 1), " pips",
         " TickValue=", DoubleToString(tickValue, 8),
         " TickSize=", DoubleToString(tickSize, Digits),
         " LossPerLot=", DoubleToString(lossPerLot, 2),
         " RawLots=", DoubleToString(rawLots, 4),
         " Lots=", DoubleToString(lots, 2),
         " ExpectedLoss=", DoubleToString(expectedLoss, 2),
         " ExpectedRisk=", DoubleToString(expectedRiskPct, 3), "%");

   return(lots);
}

//+------------------------------------------------------------------+
//| Normalize lot to broker limits                                   |
//+------------------------------------------------------------------+
double NormalizeLot(double lots)
{
   double minLot  = MarketInfo(Symbol(), MODE_MINLOT);
   double maxLot  = MarketInfo(Symbol(), MODE_MAXLOT);
   double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);

   if(minLot <= 0.0)
      minLot = 0.01;

   if(maxLot <= 0.0)
      maxLot = minLot;

   if(lotStep <= 0.0)
      lotStep = 0.01;

   // Do not round a risk-based position UP to the broker minimum.
   // If the requested risk is too small for the minimum lot, skip the trade.
   if(lots + 1e-12 < minLot)
   {
      Print("Risk lot skipped: calculated lot is below broker minimum.",
            " RawLots=", DoubleToString(lots, 4),
            " MinLot=", DoubleToString(minLot, 4));
      return(0.0);
   }

   if(lots > maxLot)
      lots = maxLot;

   lots = MathFloor((lots + 1e-12) / lotStep) * lotStep;

   if(lots + 1e-12 < minLot)
      return(0.0);

   int lotDigits = 2;

   if(lotStep >= 1.0)
      lotDigits = 0;
   else if(lotStep >= 0.1)
      lotDigits = 1;
   else if(lotStep >= 0.01)
      lotDigits = 2;
   else
      lotDigits = 3;

   return(NormalizeDouble(lots, lotDigits));
}

//+------------------------------------------------------------------+
//| Pip size helper                                                  |
//| 5-digit / 3-digit FX: 1 pip = 10 points                          |
//+------------------------------------------------------------------+
double PipSize()
{
   if(Digits == 3 || Digits == 5)
      return(Point * 10.0);

   return(Point);
}
//+------------------------------------------------------------------+

過去検証|勝率95%のEAは長期では通用しなかった

ここからは、この記事を最初に公開した2020年当時の高勝率EA検証です。

当時は「高勝率EAはどうやって作るのか?」という疑問から、ストキャスティクスとRSIを使った逆張りEAを作成しました。

利確2.5pips・損切り17pipsという損大利小の設定で、2005年1月10日~2006年10月10日の5分足をバックテストしたところ、驚くほど良い結果になりました。

旧高勝率EAの2005年から2006年のバックテスト結果
※MetaQuotes社のヒストリカルデータでバックテストした当時の結果です。
項目旧EAの結果
期間2005年1月10日~2006年10月10日
時間足M5
取引数37,021回
勝率約95%
PF2.72
平均利確約2.5pips
平均損失約17pips

数字だけ見ると、今回のV1よりも旧EAの方が圧倒的に優秀です。取引数も37,021回あり、当時はかなり期待できるように見えました。

ところが、2006年10月以降は同じEAがほとんど通用しなくなりました。

旧高勝率EAが2007年以降に成績悪化したバックテスト結果

勝率も約95%から約88%へ下がりました。88%だけを見るとまだ高勝率ですが、利小損大のEAではこの数%の低下が大きく効きます。

つまり、「勝率が高い」という一点だけではEAの強さは判断できないということです。

高勝率EAを作るときに大切だと感じたこと

旧EAの検証と今回のV1を比べると、EA作りで大切なのは「一番きれいなバックテストを作ること」ではないと感じます。

今回も開発期間だけならPF1.20でしたが、未使用期間を含めると通年PFは1.18でした。そこで数値を戻すために追加調整するのではなく、未使用期間でも大きく崩れなかった状態をそのまま採用しています。

今回の結論
高勝率EAは、TPを小さくしてSLを大きくするだけでも勝率を上げられます。
ただし本当に重要なのは、勝率だけでなくPF・ドローダウン・取引数を確認し、さらに調整に使っていない期間でも同じ考え方が通用するかを見ることです。

まとめ|スキャルピングEAは作った後の検証が重要

今回のAIスキャルピングEA V1は、USDJPYの5分足を対象に、EMA・ストキャスティクス・ATRを組み合わせて作成しました。

最終結果は698取引・勝率85.39%・PF1.18・最大DD2.89%です。

旧EAのような勝率95%・PF2.72という派手な数字ではありませんが、開発期間とは別に未使用期間を設け、スプレッドを変えた確認も行いました。

EAを自作するときは、「もっと勝率を上げる」だけでなく、条件を一つずつ変えて何が効いているのか確認することが大切です。今回のソースコードも、EMAやストキャスティクスの数値を変更しながらバックテストすると、ロジックと成績の関係がわかりやすいと思います。

コメント

タイトルとURLをコピーしました