💡 本ロジックの対象環境
対象通貨ペア:GBPUSD(ポンドドル) / 使用時間足:5分足専用
自動売買システムの開発は困難な道のりです。多くのロジックが短期的な利益に惑わされ、数ヶ月で破綻します。今回は、10年という過酷な全期間バックテストを生き抜いたロジックを公開します。成績は控えめですが、生存能力に特化した設計です。

もともと私はブレイクアウト手法の「だまし」に悩み続けてきました。多くのインジケーターを組み合わせても、レンジ相場で資金を削られる経験を繰り返しました。そこで、単なる突破ではなく「エネルギーの凝縮(スクイーズ)」と「トレンドの完全一致」に注目しました。試行錯誤の結果、生存率を極限まで高めたこのロジックに到達しました。

🤖
完璧を求めすぎると、結局カーブフィッティング(過剰最適化)に陥るのがEA開発の罠ですね。

このコードをベースにすれば、MT5のEAをゼロから開発する数百時間を節約できます。ロジックの組み方やフィルターの掛け方は、実戦的な土台として最適です。自分だけのオリジナルEAを作るための研究素材として活用してください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。

項目 実績値
プロフィットファクター (PF) 1.21
勝率 53.0%
総取引数 202 回
純利益 (0.1ロット/1万通貨時) $595.24
最大ドローダウン 4.24%
リカバリーファクター 1.40
期待利得 (Expected Payoff) 0.57

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

このロジックの最大の弱点は、利益の伸び悩みです。PF1.21という数値は、実運用では非常に心許ない水準といえます。これは、エントリー条件を厳選しすぎたためです。

低期待値のトレードを排除した結果、取引回数が激減しました。その結果、大きなトレンドを逃す場面が多々あります。また、固定の利確・損切り幅に依存しているため、相場のボラティリティ変動に柔軟に対応できていません。

ロジックの技術的詳細

本ロジックは、上位足の環境認識と執行足のエネルギー凝縮を組み合わせた戦略です。

項目 設定・条件 役割
上位足 (1h) トレンド EMA200 < EMA50 < 価格 かつ ADX > 30 強力なトレンド方向の限定
エネルギー凝縮 BBがKCの内部に位置する状態が10本中6本以上 爆発前の溜め(スクイーズ)を検知
ブレイクアウト ドンチアン25本高値/安値を更新 意識される価格帯の突破
インパルス判定 足の実体 > 直近10本の平均実体 勢いのある本物の動きを抽出
執行足 (5m) 指標 ADX > 25 かつ 上昇傾向 トレンド発生の確定
時間帯制限 15:00 〜 22:00 (サーバー時間) 流動性の高い欧州・NY時間に限定
ポイントは「H1で方向を決め、M5で溜めを待ち、勢いが出た瞬間に乗る」という多層的なフィルター構造にあります。

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

このコードはあくまで「破綻しない土台」です。ここから収益性を高めるには、以下の改善が考えられます。

  • ダイナミックな利確設定: 固定pipsではなく、ATR(平均真域)に基づいた利確幅への変更。
  • 時間帯の最適化: 通貨ペアごとのボラティリティピークを再分析し、フィルター時間を調整すること。
  • マルチタイムフレームの深化: 15分足のトレンド方向を中継させることで、エントリー精度をさらに向上させる手法。

プロのEAは、こうした環境認識を独自のロジックで補完しています。ぜひこのコードを改造して、あなただけの最適解を見つけてください。

Pythonソースコード

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

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

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

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

class VolatilityCompressionExpansionStrategy(BaseStrategy):
    def __init__(self):
        # 【クオンツ外科処置:利益効率の極大化】
        # PF 1.04 → 1.20以上、リカバリーファクター 0.51 → 3.00への飛躍的な改善を目指す。
        # Max DDに余裕がある(7.5%)ため、リスクリワード比を3.3:1へ引き上げ、1回あたりの期待利得を最大化。
        # 取引回数が多すぎる(837回)ため、エントリー条件を「超厳選」し、低期待値のトレードを徹底排除する。
        super().__init__(
            name="VolatilityCompressionExpansion_V3",
            default_tp_pips=100.0, 
            default_sl_pips=30.0,
            enable_trailing_stop=True,
            trail_start_pips=30.0
        )
        # 【ルール5】ベース時間足の定義
        self.base_timeframe = "5m"
        # 【ルール6】画像認識AI用時間足の定義
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        # --- 1. 上位足(1h)の環境認識 (ルックアヘッドバイアス完全排除) ---
        h1_df = df.resample('1h').agg({
            'Open': 'first',
            'High': 'max',
            'Low': 'min',
            'Close': 'last'
        })
        
        # 【処方箋】トレンド判定の重層化:EMA200(長期)+ EMA50(中期)のパーフェクトオーダーを要求
        h1_ema200 = ta.ema(h1_df['Close'], length=200)
        h1_ema50 = ta.ema(h1_df['Close'], length=50)
        
        # H1 ADX: 閾値を30に引き上げ、「最強のトレンド」のみを抽出
        h1_adx_df = ta.adx(h1_df['High'], h1_df['Low'], h1_df['Close'], length=14)
        h1_adx = h1_adx_df['ADX_14'] if h1_adx_df is not None else None
        
        # 強気:価格 > EMA50 > EMA200 かつ ADX > 30
        h1_bull = (h1_df['Close'] > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_adx > 30)
        # 弱気:価格 < EMA50 < EMA200 かつ ADX > 30
        h1_bear = (h1_df['Close'] < h1_ema50) & (h1_ema50 < h1_ema200) & (h1_adx > 30)
        
        df['h1_bull'] = h1_bull.shift(1).reindex(df.index, method='ffill')
        df['h1_bear'] = h1_bear.shift(1).reindex(df.index, method='ffill')

        # --- 2. 執行足(5m)のテクニカル計算 ---
        # ボリンジャーバンド (BB) - iloc参照でエラー回避
        bb = ta.bbands(df['Close'], length=20, std=2.0)
        df['bb_lower'] = bb.iloc[:, 0]
        df['bb_upper'] = bb.iloc[:, 2]
        
        # ケルトナーチャネル (KC) - iloc参照でエラー回避
        kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=2.0)
        df['kc_lower'] = kc.iloc[:, 0]
        df['kc_upper'] = kc.iloc[:, 2]
        
        # Squeeze判定
        df['is_squeeze'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
        
        # 【処方箋】だまし排除:Squeeze継続期間を10本中6本以上に厳格化し、エネルギー凝縮を確実にする
        df['squeeze_count'] = df['is_squeeze'].rolling(window=10).sum()
        
        # 【処方箋】ドンチアンチャネル期間を25に延長し、より強力なブレイクアウトのみを捕捉
        df['donchian_high'] = df['High'].rolling(window=25).max().shift(1)
        df['donchian_low'] = df['Low'].rolling(window=25).min().shift(1)
        
        # 執行足 ADX: 閾値を25に引き上げ
        adx_m5 = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['adx_m5'] = adx_m5['ADX_14'] if adx_m5 is not None else 0
        
        # 【処方箋】インパルス判定:ブレイクした足の実体が直近平均以上のサイズであることを要求
        df['body_size'] = (df['Close'] - df['Open']).abs()
        df['avg_body_size'] = df['body_size'].rolling(window=10).mean()
        
        # 時間帯フィルター (欧州・NYの最活発時間帯に限定)
        df['hour'] = df.index.hour
        df['time_filter'] = (df['hour'] >= 15) & (df['hour'] <= 22)
        
        return df

    def generate_signal(self, df):
        # 最新の行と過去の行を取得
        last = df.iloc[-1]
        prev = df.iloc[-2] if len(df) > 1 else last
        
        # --- 買いシグナル条件 ---
        buy_signal = (
            (last['h1_bull'] == True) and
            (prev['squeeze_count'] >= 6) and 
            (last['Close'] > last['donchian_high']) and
            (last['Close'] > last['bb_upper']) and
            (last['body_size'] > last['avg_body_size']) and
            (last['adx_m5'] > 25) and
            (last['adx_m5'] > prev['adx_m5']) and
            (last['time_filter'] == True)
        )
        
        # --- 売りシグナル条件 ---
        sell_signal = (
            (last['h1_bear'] == True) and
            (prev['squeeze_count'] >= 6) and
            (last['Close'] < last['donchian_low']) and
            (last['Close'] < last['bb_lower']) and
            (last['body_size'] > last['avg_body_size']) and
            (last['adx_m5'] > 25) and
            (last['adx_m5'] > prev['adx_m5']) and
            (last['time_filter'] == True)
        )
        
        if buy_signal:
            return 'BUY'
        elif sell_signal:
            return 'SELL'
            
        return None

MQL5コード(MT5用)

//+------------------------------------------------------------------+
//|                                VolatilityCompressionExpansion.mq5|
//|                                  Copyright 2026, Quants Engineer |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quants Engineer"
#property link      "https://your-blog-url.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input parameters
input int      InpTP_Pips = 100;          // Take Profit (Pips)
input int      InpSL_Pips = 30;           // Stop Loss (Pips)
input int      InpTrailStart = 30;        // Trailing Start (Pips)
input int      InpADX_Threshold_H1 = 30;  // H1 ADX Threshold
input int      InpADX_Threshold_M5 = 25;  // M5 ADX Threshold
input int      InpSqueezeMinBars = 6;     // Min Squeeze Bars in 10
input int      InpDonchianPeriod = 25;    // Donchian Period

//--- Global variables
CTrade trade;
int handle_ema200_h1, handle_ema50_h1, handle_adx_h1;
int handle_bb_m5, handle_adx_m5;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   // H1 Indicators
   handle_ema200_h1 = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
   handle_ema50_h1  = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
   handle_adx_h1    = iADX(_Symbol, PERIOD_H1, 14);
   
   // M5 Indicators
   handle_bb_m5     = iBands(_Symbol, PERIOD_M5, 20, 0, 2.0, PRICE_CLOSE);
   handle_adx_m5    = iADX(_Symbol, PERIOD_M5, 14);
   
   if(handle_ema200_h1 == INVALID_HANDLE || handle_adx_m5 == INVALID_HANDLE) return(INIT_FAILED);
   return(INIT_SUCCEEDED);
}

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

   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if(dt.hour < 15 || dt.hour > 22) return;

   // H1 Data
   double ema200_h1[], ema50_h1[], adx_h1[], close_h1[];
   CopyBuffer(handle_ema200_h1, 0, 1, 1, ema200_h1);
   CopyBuffer(handle_ema50_h1, 0, 1, 1, ema50_h1);
   CopyBuffer(handle_adx_h1, 0, 1, 1, adx_h1);
   CopyClose(_Symbol, PERIOD_H1, 1, 1, close_h1);

   bool h1_bull = (close_h1[0] > ema50_h1[0] && ema50_h1[0] > ema200_h1[0] && adx_h1[0] > InpADX_Threshold_H1);
   bool h1_bear = (close_h1[0] < ema50_h1[0] && ema50_h1[0] < ema200_h1[0] && adx_h1[0] > InpADX_Threshold_H1);

   // M5 Data
   double bb_upper[], bb_lower[], adx_m5[], close_m5[], open_m5[], high_m5[], low_m5[];
   CopyBuffer(handle_bb_m5, 1, 1, 1, bb_upper);
   CopyBuffer(handle_bb_m5, 2, 1, 1, bb_lower);
   CopyBuffer(handle_adx_m5, 0, 1, 2, adx_m5);
   CopyClose(_Symbol, PERIOD_M5, 1, 1, close_m5);
   CopyOpen(_Symbol, PERIOD_M5, 1, 1, open_m5);
   CopyHigh(_Symbol, PERIOD_M5, 1, InpDonchianPeriod, high_m5);
   CopyLow(_Symbol, PERIOD_M5, 1, InpDonchianPeriod, low_m5);

   // Donchian
   double d_high = high_m5[ArrayMaximum(high_m5)];
   double d_low = low_m5[ArrayMinimum(low_m5)];

   // Squeeze check (BB vs KC Simplified)
   int squeeze_count = 0;
   for(int i=1; i<=10; i++) {
      double b_up[], b_lo[], k_up[], k_lo[];
      // Simplified KC calculation for logic representation
      // In actual implementation, use iATR and iMA to calculate KC
      if(getBBUpper(i) < getKCUpper(i) && getBBLower(i) > getKCLower(i)) squeeze_count++;
   }

   // Body Size
   double body = MathAbs(close_m5[0] - open_m5[0]);
   double avg_body = getAvgBody(10);

   // Entry Logic
   if(h1_bull && squeeze_count >= InpSqueezeMinBars && close_m5[0] > d_high && close_m5[0] > bb_upper[0] && body > avg_body && adx_m5[0] > InpADX_Threshold_M5 && adx_m5[0] > adx_m5[1]) {
      double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL_Pips * _Point * 10;
      double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP_Pips * _Point * 10;
      trade.Buy(0.1, _Symbol, 0, sl, tp);
   }
   else if(h1_bear && squeeze_count >= InpSqueezeMinBars && close_m5[0] < d_low && close_m5[0] < bb_lower[0] && body > avg_body && adx_m5[0] > InpADX_Threshold_M5 && adx_m5[0] > adx_m5[1]) {
      double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL_Pips * _Point * 10;
      double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP_Pips * _Point * 10;
      trade.Sell(0.1, _Symbol, 0, sl, tp);
   }

   manageTrailingStop();
}

//--- Helper functions for KC and Body (Abstracted for brevity)
double getBBUpper(int shift) { double b[]; CopyBuffer(handle_bb_m5, 1, shift, 1, b); return b[0]; }
double getBBLower(int shift) { double b[]; CopyBuffer(handle_bb_m5, 2, shift, 1, b); return b[0]; }
double getKCUpper(int shift) { 
   double ma[]; double atr[]; 
   int h_ma = iMA(_Symbol, PERIOD_M5, 20, 0, MODE_SMA, PRICE_CLOSE);
   int h_atr = iATR(_Symbol, PERIOD_M5, 20);
   CopyBuffer(h_ma, 0, shift, 1, ma); CopyBuffer(h_atr, 0, shift, 1, atr);
   return ma[0] + 2.0 * atr[0];
}
double getKCLower(int shift) { 
   double ma[]; double atr[]; 
   int h_ma = iMA(_Symbol, PERIOD_M5, 20, 0, MODE_SMA, PRICE_CLOSE);
   int h_atr = iATR(_Symbol, PERIOD_M5, 20);
   CopyBuffer(h_ma, 0, shift, 1, ma); CopyBuffer(h_atr, 0, shift, 1, atr);
   return ma[0] - 2.0 * atr[0];
}
double getAvgBody(int period) {
   double sum = 0;
   for(int i=1; i<=period; i++) {
      double o[], c[];
      CopyOpen(_Symbol, PERIOD_M5, i, 1, o); CopyClose(_Symbol, PERIOD_M5, i, 1, c);
      sum += MathAbs(c[0] - o[0]);
   }
   return sum / period;
}
bool isNewBar() {
   static datetime last_bar = 0;
   datetime curr_bar = iTime(_Symbol, PERIOD_M5, 0);
   if(last_bar != curr_bar) { last_bar = curr_bar; return true; }
   return false;
}
void manageTrailingStop() {
   for(int i=PositionsTotal()-1; i>=0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket)) {
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
            double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTrailStart * _Point * 10;
            if(new_sl > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
         } else {
            double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpTrailStart * _Point * 10;
            if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
         }
      }
   }
}