💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
今回は、8年半のバックテストを破綻せずに生き抜いたロジックをご紹介します。長期的な生存能力は証明されました。一方で、成績が伸び悩んだ点や特有の弱点も明確に現れています。

バックテスト資産推移

これが過酷な全期間8年半の市場変動を破綻せず生き抜いた資産推移グラフ(長期検証証拠)です。

項目 実績値
プロフィットファクター (PF) 1.21
勝率 46.5%
総取引数 527 回
純利益 (0.1ロット/1万通貨時) $96888.54
最大ドローダウン 3.93%

今回の検証結果の「限界」と「ダメ出し」

PF1.21という数値は、実運用では物足りない成績です。勝率が50%を切っているため、連敗時の心理的負荷は避けられません。

この要因は、フィルターを厳格にしすぎたことにあります。ボラティリティ圧縮(ATRパーセンタイル)を重視したため、絶好のトレンド初動を逃した局面が散見されました。また、5分足という短い時間軸ゆえに、ノイズによる損切り回数が増えています。

結果として、最大ドローダウンを3.93%に抑えることには成功しました。しかし、攻めの姿勢が欠けており、機会損失を許容しすぎた設計と言わざるを得ません。

🤖
安全性を重視してリスクを削ぎ落とした結果、爆発力が消えてしまった印象です。

ロジックの技術的詳細

本ロジックは、ボラティリティの「圧縮」と「爆発」を数値化して狙う戦略です。

指標 設定値 役割
KAMA 期間 21 価格の適応的平滑化とトレンド方向の判定
ADX 期間 14 / 閾値 25 トレンドの強度を測定し、弱い相場を除外
ER (効率比) 期間 14 / 閾値 0.4 価格変動の効率性を測定し、急騰・急落を検知
ATR Percentile 期間 100 / 閾値 0.3 ボラティリティが極めて低い「スクイーズ」を判定
SuperTrend 15分足 / 12 / 3.0 上位足のトレンド方向へのみエントリーを制限
時間フィルター 16:00 - 23:00 JST 流動性の高い欧州・NY市場のみに限定

どう改善すべきか(次なる展望)

このロジックをさらに進化させるには、価格以外の視点が必要です。

例えば、出来高(Volume)の急増を条件に加えることが考えられます。真のブレイクアウトは、出来高を伴うものです。現在のATRベースの判定に、出来高フィルターを組み合わせれば、ダマシをさらに減らせる可能性があります。

また、固定のTP/SLではなく、直近の高値・安値に基づいた動的な決済ロジックへの変更も有効です。プロのEAは、相場のボラティリティに応じて利確幅を変動させます。

このコードは完成品ではなく、研究用の「原石」です。上位足の環境認識や、独自のフィルターを付け足して最適化することをおすすめします。

Pythonコード

🔍 プロはどうやってダマシを回避しているのか?

今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。

👉 プロのフィルターロジックが詰まった実績EA(White Bear Z USDJPY)を参考にする

from strategies.base import BaseStrategy
import pandas as pd
import pandas_ta as ta

class AEISStrategy(BaseStrategy):
    def __init__(self):
        # 【クオンツ調整】リカバリーファクターと最大DDの改善のため、
        # リスクリワード比を最適化(3.33 -> 2.25)し、勝率の底上げを図る
        super().__init__(
            name="Adaptive Efficiency Impulse Strategy",
            default_tp_pips=45.0,
            default_sl_pips=20.0,
            enable_trailing_stop=True,
            trail_start_pips=15.0
        )
        # 時間足の定義
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        【クオンツ調整】
        - KAMA期間を21に延長し、ノイズによるダマシを軽減 (PF向上)
        - ER閾値とADX閾値を引き上げ、より高精度なインパルスのみを抽出 (勝率向上)
        - ATRパーセンタイル閾値を下げ、ボラティリティ圧縮の状態を厳格化 (最大DD抑制)
        """
        # --- 1. 下位足 (5m) 指標計算 ---
        # KAMA: 期間を10 -> 21へ変更して平滑化
        df['KAMA'] = ta.kama(df['Close'], length=21)
        
        # ADX: トレンド強度判定
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['ADX'] = adx_df['ADX_14'] if adx_df is not None else 0
        
        # ATR: ボラティリティ計算 (KeyError回避のため明示的に代入)
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        
        # Efficiency Ratio (ER): 期間を10 -> 14へ変更し、相関性を向上
        change = df['Close'].diff(14).abs()
        volatility = df['Close'].diff().abs().rolling(14).sum()
        df['ER'] = change / volatility
        
        # ATR Percentile: 閾値を0.5 -> 0.3へ。より強い「圧縮」を条件とする
        df['ATR_pct'] = df['ATR'].rolling(100).rank(pct=True)

        # --- 2. 上位足 (15min) トレンド判定 ---
        # ルックアヘッドバイアス排除のため shift(1) を徹底
        df_15 = df[['High', 'Low', 'Close']].resample('15min').last().shift(1)
        
        # SuperTrend: 期間12, 倍率3.0に調整してトレンド追随性を最適化
        st_15 = ta.supertrend(df_15['High'], df_15['Low'], df_15['Close'], length=12, multiplier=3.0)
        
        if st_15 is not None:
            # SuperTrendの方向性カラム(通常はSUPERTd_12_3.0)を抽出
            # カラム名に依存せず、方向性を示す2番目のカラム(index 1)を取得
            trend_col = st_15.columns[1]
            df['trend_15'] = st_15[trend_col].reindex(df.index, method='ffill')
        else:
            df['trend_15'] = 0

        return df

    def generate_signal(self, df):
        """
        【クオンツ調整】
        - ADX > 25: トレンドの確信度を高める
        - ER > 0.4: 効率性の爆発条件を厳格化
        - ATR_pct < 0.3: 真のスクイーズからのブレイクアウトを狙う
        """
        if len(df) < 100:
            return None

        last_row = df.iloc[-1]
        
        # 必須データの存在確認
        required_cols = ['KAMA', 'ADX', 'ATR', 'ER', 'ATR_pct', 'trend_15']
        if any(col not in last_row for col in required_cols):
            return None

        # 時間帯フィルター (16:00 - 24:00 JST)
        hour = df.index[-1].hour
        time_filter = (16 <= hour <= 23)

        # --- エントリー条件の再構築 ---
        # 1. MTF同期: 上位足トレンド方向
        upper_bull = last_row['trend_15'] == 1
        upper_bear = last_row['trend_15'] == -1

        # 2. 効率性の爆発: ER閾値を 0.3 -> 0.4 に引き上げ
        er_explosion = last_row['ER'] > 0.4

        # 3. 適応的価格突破: 価格 vs KAMA
        price_above_kama = last_row['Close'] > last_row['KAMA']
        price_below_kama = last_row['Close'] < last_row['KAMA']

        # 4. ボラティリティ圧縮: ATRパーセンタイルを 0.5 -> 0.3 に引き下げ
        vol_squeeze = last_row['ATR_pct'] < 0.3

        # 5. 勢い判定: ADX閾値を 20 -> 25 に引き上げ
        trend_strength = last_row['ADX'] > 25

        if time_filter:
            # ロング条件: 全てのクオンツフィルターを通過
            if upper_bull and er_explosion and price_above_kama and vol_squeeze and trend_strength:
                return 'BUY'
            
            # ショート条件: 全てのクオンツフィルターを通過
            if upper_bear and er_explosion and price_below_kama and vol_squeeze and trend_strength:
                return 'SELL'

        return None

MQL5コード

//+------------------------------------------------------------------+
//|                                              AEIS_Strategy.mq5   |
//|                                  Copyright 2026, Quant Engineer  |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- 入力パラメータ
input int      InpKAMA_Period = 21;          // KAMA Period
input int      InpADX_Period  = 14;          // ADX Period
input double   InpADX_Level   = 25.0;        // ADX Threshold
input int      InpER_Period   = 14;          // Efficiency Ratio Period
input double   InpER_Level    = 0.4;         // ER Threshold
input int      InpATR_Period  = 14;          // ATR Period
input double   InpATR_Pct     = 0.3;         // ATR Percentile Threshold
input int      InpST_Period   = 12;          // SuperTrend Period
input double   InpST_Mult     = 3.0;         // SuperTrend Multiplier
input double   InpTP_Pips     = 45.0;        // Take Profit (Pips)
input double   InpSL_Pips     = 20.0;        // Stop Loss (Pips)
input double   InpTrailStart  = 15.0;        // Trailing Start (Pips)
input double   InpLotSize     = 0.1;         // Lot Size

//--- グローバル変数
CTrade trade;
int handleADX, handleATR;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handleADX = iADX(_Symbol, _Period, InpADX_Period);
   handleATR = iATR(_Symbol, _Period, InpATR_Period);
   
   if(handleADX == INVALID_HANDLE || handleATR == INVALID_HANDLE)
      return(INIT_FAILED);
      
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| KAMA calculation                                                 |
//+------------------------------------------------------------------+
double CalculateKAMA(int period)
{
   double close[];
   ArraySetAsSeries(close, true);
   CopyClose(_Symbol, _Period, 0, period * 5, close);
   
   double change = MathAbs(close[0] - close[period]);
   double volatility = 0;
   for(int i=0; i<period; i++) volatility += MathAbs(close[i] - close[i+1]);
   
   double er = (volatility != 0) ? change / volatility : 0;
   double sc = MathPow(er * (2.0 / (3.0) - 1.0) + 1.0, 2);
   
   static double lastKAMA = 0;
   if(lastKAMA == 0) lastKAMA = close[0];
   
   double currentKAMA = lastKAMA + sc * (close[0] - lastKAMA);
   lastKAMA = currentKAMA;
   return currentKAMA;
}

//+------------------------------------------------------------------+
//| Efficiency Ratio (ER) calculation                                |
//+------------------------------------------------------------------+
double CalculateER(int period)
{
   double close[];
   ArraySetAsSeries(close, true);
   CopyClose(_Symbol, _Period, 0, period + 1, close);
   
   double change = MathAbs(close[0] - close[period]);
   double volatility = 0;
   for(int i=0; i<period; i++) volatility += MathAbs(close[i] - close[i+1]);
   
   return (volatility != 0) ? change / volatility : 0;
}

//+------------------------------------------------------------------+
//| SuperTrend for Higher Timeframe (15m)                            |
//+------------------------------------------------------------------+
int GetSuperTrend(ENUM_TIMEFRAMES tf)
{
   double high[], low[], close[];
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);
   
   CopyHigh(_Symbol, tf, 0, 2, high);
   CopyLow(_Symbol, tf, 0, 2, low);
   CopyClose(_Symbol, tf, 0, 2, close);
   
   double atr[];
   int hATR = iATR(_Symbol, tf, InpATR_Period);
   CopyBuffer(hATR, 0, 0, 1, atr);
   
   double upperBand = (high[0] + low[0]) / 2 + InpST_Mult * atr[0];
   double lowerBand = (high[0] + low[0]) / 2 - InpST_Mult * atr[0];
   
   if(close[0] > upperBand) return 1;
   if(close[0] < lowerBand) return -1;
   return 0;
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if(dt.hour < 16 || dt.hour > 23) return;

   if(PositionsTotal() > 0) 
   {
      ManageTrailingStop();
      return;
   }

   double adx[], atr[];
   CopyBuffer(handleADX, 0, 0, 1, adx);
   CopyBuffer(handleATR, 0, 0, 1, atr);
   
   double kama = CalculateKAMA(InpKAMA_Period);
   double er = CalculateER(InpER_Period);
   int trend15 = GetSuperTrend(PERIOD_M15);
   
   // ATR Percentile (Simplified: check if current ATR is below average of last 100)
   double atrSum = 0;
   double atrBuffer[];
   CopyBuffer(handleATR, 0, 0, 100, atrBuffer);
   for(int i=0; i<100; i++) atrSum += atrBuffer[i];
   double atrAvg = atrSum / 100;
   bool volSqueeze = (atr[0] < atrAvg * 0.7); // Approximate 0.3 percentile

   double close = iClose(_Symbol, _Period, 0);

   // Long Entry
   if(trend15 == 1 && er > InpER_Level && close > kama && volSqueeze && adx[0] > InpADX_Level)
   {
      double sl = close - InpSL_Pips * _Point * 10;
      double tp = close + InpTP_Pips * _Point * 10;
      trade.Buy(InpLotSize, _Symbol, close, sl, tp);
   }
   // Short Entry
   else if(trend15 == -1 && er > InpER_Level && close < kama && volSqueeze && adx[0] > InpADX_Level)
   {
      double sl = close + InpSL_Pips * _Point * 10;
      double tp = close - InpTP_Pips * _Point * 10;
      trade.Sell(InpLotSize, _Symbol, close, sl, tp);
   }
}

//+------------------------------------------------------------------+
//| Trailing Stop Logic                                              |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
   for(int i=PositionsTotal()-1; i>=0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
      {
         double price = PositionGetDouble(POSITION_PRICE_OPEN);
         double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
         double sl = PositionGetDouble(POSITION_SL);
         
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            if(currentPrice - price > InpTrailStart * _Point * 10)
            {
               double newSL = currentPrice - InpTrailStart * _Point * 10;
               if(newSL > sl) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         }
         else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
         {
            if(price - currentPrice > InpTrailStart * _Point * 10)
            {
               double newSL = currentPrice + InpTrailStart * _Point * 10;
               if(sl == 0 || newSL < sl) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}