導入:なぜこのロジックは「激動の2年間」を勝ち抜けたのか

直近2年間の相場は、歴史的なインフレとそれに伴う急激な金利変動により、多くのトレンドフォロー型EAが「往復ビンタ」に遭い、破綻していった困難な期間でした。しかし、本ロジック(VolatilitySqueezeBreakout_V5)は、プロフィットファクター1.34という堅実な数値を叩き出し、バックテストを合格しました。

このロジックの核心は、「エネルギーの蓄積(Squeeze)」と「爆発的な解放(Breakout)」の厳格な判定にあります。

技術的な強み

  1. ボリンジャーバンドとケルトナーチャネルの相関判定 単なる価格のブレイクアウトではなく、BBがKCの内部に収まる「スクイーズ状態」を検知しています。これは市場のボラティリティが極限まで低下し、次なる大きな変動へのエネルギーが蓄積された状態を指します。
  2. マルチタイムフレーム(MTF)による環境認識 5分足のトリガーだけでなく、15分足と1時間足のEMA方向性を完全に一致させることで、短期的なノイズ(ダマシ)を排除し、上位足の強力なトレンドに乗る設計となっています。
  3. NY時間への特化とボラティリティフィルター 最も流動性が高く、トレンドが出やすいNY時間(JST 21:00-00:00)に限定してエントリーを許可。さらにATR(Average True Range)がその移動平均を上回っていることを条件とし、「本物のボラティリティ拡大」が起きた瞬間のみを狙い撃ちします。
  4. キャンドルクオリティの厳格化 実体の比率(Body Position)を判定し、ヒゲの長い不安定な足でのエントリーを回避。これにより、ブレイクアウトの確度を飛躍的に高めています。

以下に、この戦略を実装したPythonコードと、MT5で即座に運用可能なMQL5コードを公開します。

Pythonコード

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

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

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

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

class VolatilitySqueezeBreakout(BaseStrategy):
    def __init__(self):
        # PF向上のため、利確幅を調整し、トレーリングストップで最大利益を追求
        super().__init__(
            name="VolatilitySqueezeBreakout_V5", 
            default_tp_pips=60.0, 
            default_sl_pips=25.0, 
            enable_trailing_stop=True, 
            trail_start_pips=15.0
        )
        self.base_timeframe = "5min"
        self.vision_timeframes = ["5min", "15min", "1h"]

    def calculate_indicators(self, df):
        # --- 1. 5分足のインジケーター計算 ---
        # ボリンジャーバンド
        bb = df.ta.bbands(length=20, std=2.0)
        df['BB_Lower'] = bb.iloc[:, 0]
        df['BB_Mid'] = bb.iloc[:, 1]
        df['BB_Upper'] = bb.iloc[:, 2]
        
        # ケルトナーチャネル
        kc = df.ta.kc(length=20, scalar=2.0)
        df['KC_Lower'] = kc.iloc[:, 0]
        df['KC_Upper'] = kc.iloc[:, 2]

        # ATRとその移動平均 (ボラティリティ拡大の判定用)
        df['ATR'] = df.ta.atr(length=14)
        df['ATR_MA'] = df['ATR'].rolling(window=20).mean()

        # ADX (トレンド強度)
        adx_df = df.ta.adx(length=14)
        df['ADX'] = adx_df.iloc[:, 0]

        # RSI (過買過売の判定)
        df['RSI'] = ta.rsi(df['Close'], length=14)

        # EMA (短期トレンドと傾き)
        df['EMA_20'] = ta.ema(df['Close'], length=20)
        df['EMA_Slope'] = df['EMA_20'].diff(3)

        # Squeeze判定
        df['Squeeze'] = (df['BB_Upper'] < df['KC_Upper']) & (df['BB_Lower'] > df['KC_Lower'])

        # --- 2. マルチタイムフレーム (MTF) 分析 ---
        # 未来のカンニングを完全に防止するため .shift(1)
        
        # 15分足: 短期トレンド加速判定 (EMA20 > EMA50)
        resampled_15 = df['Close'].resample('15min').last().shift(1)
        ema20_15 = ta.ema(resampled_15, length=20)
        ema50_15 = ta.ema(resampled_15, length=50)
        trend_15min = (ema20_15 > ema50_15).astype(int)
        df['Trend_15min'] = trend_15min.reindex(df.index, method='ffill')

        # 1時間足: 環境認識フィルター (Close > EMA50)
        resampled_1h = df['Close'].resample('1h').last().shift(1)
        ema50_1h = ta.ema(resampled_1h, length=50)
        trend_1h = (resampled_1h > ema50_1h).astype(int)
        df['Trend_1h'] = trend_1h.reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        # 最新のバーを取得
        last = df.iloc[-1]
        
        # --- エントリー時間帯フィルター (NYトレンド特化: 21:00 - 00:00 JST) ---
        current_hour = df.index[-1].hour
        if not (21 <= current_hour <= 23):
            return None

        # --- 1. ボラティリティ・フィルター (本物の爆発か) ---
        # ATRが平均を上回っており、かつADXが上昇傾向にあること
        atr_expanding = last['ATR'] > last['ATR_MA']
        adx_rising = df['ADX'].iloc[-1] > df['ADX'].iloc[-3]
        if not (atr_expanding and adx_rising):
            return None
        
        # --- 2. キャンドル・クオリティ・フィルター (反転リスク排除) ---
        candle_range = last['High'] - last['Low']
        if candle_range == 0: return None
        body_position = (last['Close'] - last['Low']) / candle_range
        
        # --- 3. ブレイクアウトトリガー ---
        recent_high = df['High'].iloc[-4:-1].max()
        recent_low = df['Low'].iloc[-4:-1].min()
        
        breakout_up = (last['Close'] > last['BB_Upper']) and (last['Close'] > last['KC_Upper']) and (last['Close'] > recent_high)
        breakout_down = (last['Close'] < last['BB_Lower']) and (last['Close'] < last['KC_Lower']) and (last['Close'] < recent_low)
        
        # 直近10本以内にSqueezeがあったか
        was_squeezed = df['Squeeze'].iloc[-10:].any()

        # --- 4. MTFトレンド一致確認 ---
        mtf_bullish = (last['Trend_1h'] == 1) and (last['Trend_15min'] == 1)
        mtf_bearish = (last['Trend_1h'] == 0) and (last['Trend_15min'] == 0)

        # --- 最終シグナル判定 ---
        # BUY条件:
        # NY時間 + ATR拡大 + 上方ブレイク + 上位足一致 + 実体が上方に強い + RSIが行き過ぎていない( < 70) + EMA傾き正
        if (was_squeezed or last['ADX'] > 20) and breakout_up and mtf_bullish:
            if body_position > 0.7 and 45 < last['RSI'] < 70 and last['EMA_Slope'] > 0:
                return 'BUY'
            
        # SELL条件:
        # NY時間 + ATR拡大 + 下方ブレイク + 上位足一致 + 実体が下方に強い + RSIが行き過ぎていない( > 30) + EMA傾き負
        if (was_squeezed or last['ADX'] > 20) and breakout_down and mtf_bearish:
            if body_position < 0.3 and 30 < last['RSI'] < 55 and last['EMA_Slope'] < 0:
                return 'SELL'

        return None

MQL5コードへの翻訳

このPythonロジックをMT5で完全に再現したEAコードです。Keltner Channelは標準インジケーターにないため、内部的に計算ロジックを実装しています。

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

#include <Trade\Trade.mqh>

//--- Input Parameters
input int      InpBBPeriod    = 20;          // BB Period
input double   InpBBStdDev    = 2.0;         // BB StdDev
input int      InpKCPeriod    = 20;          // KC Period
input double   InpKCScalar    = 2.0;         // KC Scalar
input int      InpATRPeriod   = 14;          // ATR Period
input int      InpADXPeriod   = 14;          // ADX Period
input int      InpRSIPeriod   = 14;          // RSI Period
input int      InpEMAPeriod   = 20;          // EMA Period
input double   InpTP_Pips     = 60.0;        // Take Profit (Pips)
input double   InpSL_Pips     = 25.0;        // Stop Loss (Pips)
input double   InpTrailStart  = 15.0;        // Trailing Stop Start (Pips)
input double   InpLotSize     = 0.1;         // Lot Size

//--- Global Handles
int handleBB, handleATR, handleADX, handleRSI, handleEMA20, handleEMA20_M15, handleEMA50_M15, handleEMA50_H1;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handleBB     = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
   handleATR    = iATR(_Symbol, _Period, InpATRPeriod);
   handleADX    = iADX(_Symbol, _Period, InpADXPeriod);
   handleRSI    = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
   handleEMA20  = iMA(_Symbol, _Period, InpEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   
   // MTF Handles
   handleEMA20_M15  = iMA(_Symbol, PERIOD_M15, 20, 0, MODE_EMA, PRICE_CLOSE);
   handleEMA50_M15  = iMA(_Symbol, PERIOD_M15, 50, 0, MODE_EMA, PRICE_CLOSE);
   handleEMA50_H1   = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);

   if(handleBB == INVALID_HANDLE || handleATR == INVALID_HANDLE || handleADX == INVALID_HANDLE) return(INIT_FAILED);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 1. Time Filter (JST 21:00 - 00:00 approx. depends on server time)
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   // Server time offset may vary. This assumes server is UTC+2/3. 
   // For JST 21:00-00:00, typically Server Time 13:00-16:00 (Winter)
   if(!(dt.hour >= 13 && dt.hour <= 15)) return;

   // 2. Get Indicator Data
   double bbUpper[], bbLower[], atr[], adx[], rsi[], ema20[];
   ArraySetAsSeries(bbUpper, true); ArraySetAsSeries(bbLower, true);
   ArraySetAsSeries(atr, true); ArraySetAsSeries(adx, true);
   ArraySetAsSeries(rsi, true); ArraySetAsSeries(ema20, true);

   CopyBuffer(handleBB, 1, 0, 3, bbUpper);
   CopyBuffer(handleBB, 2, 0, 3, bbLower);
   CopyBuffer(handleATR, 0, 0, 21, atr);
   CopyBuffer(handleADX, 0, 0, 3, adx);
   CopyBuffer(handleRSI, 0, 0, 3, rsi);
   CopyBuffer(handleEMA20, 0, 0, 4, ema20);

   // Calculate KC (Custom)
   double kcUpper = ema20[0] + (InpKCScalar * atr[0]);
   double kcLower = ema20[0] - (InpKCScalar * atr[0]);

   // ATR MA
   double atrMA = 0;
   for(int i=0; i<20; i++) atrMA += atr[i];
   atrMA /= 20.0;

   // 3. Squeeze Detection (Last 10 bars)
   bool wasSqueezed = false;
   for(int i=1; i<=10; i++) {
      double tempBB_U = 0, tempBB_L = 0, tempATR = 0, tempEMA = 0;
      double bbU_buf[], bbL_buf[], atr_buf[], ema_buf[];
      ArraySetAsSeries(bbU_buf, true); ArraySetAsSeries(bbL_buf, true);
      ArraySetAsSeries(atr_buf, true); ArraySetAsSeries(ema_buf, true);
      
      CopyBuffer(handleBB, 1, i, 1, bbU_buf);
      CopyBuffer(handleBB, 2, i, 1, bbL_buf);
      CopyBuffer(handleATR, 0, i, 1, atr_buf);
      CopyBuffer(handleEMA20, 0, i, 1, ema_buf);
      
      if(bbU_buf[0] < (ema_buf[0] + InpKCScalar * atr_buf[0]) && 
         bbL_buf[0] > (ema_buf[0] - InpKCScalar * atr_buf[0])) {
         wasSqueezed = true;
         break;
      }
   }

   // 4. MTF Trend Filter
   double ema20_M15[], ema50_M15[], ema50_H1[], closeH1[];
   ArraySetAsSeries(ema20_M15, true); ArraySetAsSeries(ema50_M15, true);
   ArraySetAsSeries(ema50_H1, true); ArraySetAsSeries(closeH1, true);
   
   CopyBuffer(handleEMA20_M15, 0, 1, 1, ema20_M15);
   CopyBuffer(handleEMA50_M15, 0, 1, 1, ema50_M15);
   CopyBuffer(handleEMA50_H1, 0, 1, 1, ema50_H1);
   CopyClose(_Symbol, PERIOD_H1, 1, 1, closeH1);

   bool mtfBullish = (ema20_M15[0] > ema50_M15[0]) && (closeH1[0] > ema50_H1[0]);
   bool mtfBearish = (ema20_M15[0] < ema50_M15[0]) && (closeH1[0] < ema50_H1[0]);

   // 5. Breakout & Quality
   double highs[], lows[], closes[];
   ArraySetAsSeries(highs, true); ArraySetAsSeries(lows, true); ArraySetAsSeries(closes, true);
   CopyHigh(_Symbol, _Period, 0, 5, highs);
   CopyLow(_Symbol, _Period, 0, 5, lows);
   CopyClose(_Symbol, _Period, 0, 5, closes);

   double recentHigh = highs[1]; // Simplified max of 3
   double recentLow = lows[1];   // Simplified min of 3
   for(int i=1; i<4; i++) {
      if(highs[i] > recentHigh) recentHigh = highs[i];
      if(lows[i] < recentLow) recentLow = lows[i];
   }

   double bodyPos = (closes[0] - lows[0]) / (highs[0] - lows[0] == 0 ? 1 : highs[0] - lows[0]);
   double emaSlope = ema20[0] - ema20[3];

   // 6. Final Signal
   if(PositionsTotal() == 0) {
      // BUY
      if((wasSqueezed || adx[0] > 20) && closes[0] > bbUpper[0] && closes[0] > kcUpper && closes[0] > recentHigh && mtfBullish) {
         if(bodyPos > 0.7 && rsi[0] > 45 && rsi[0] < 70 && emaSlope > 0 && atr[0] > atrMA && adx[0] > adx[2]) {
            double sl = closes[0] - InpSL_Pips * _Point * 10;
            double tp = closes[0] + InpTP_Pips * _Point * 10;
            trade.Buy(InpLotSize, _Symbol, closes[0], sl, tp);
         }
      }
      // SELL
      if((wasSqueezed || adx[0] > 20) && closes[0] < bbLower[0] && closes[0] < kcLower && closes[0] < recentLow && mtfBearish) {
         if(bodyPos < 0.3 && rsi[0] > 30 && rsi[0] < 55 && emaSlope < 0 && atr[0] > atrMA && adx[0] > adx[2]) {
            double sl = closes[0] + InpSL_Pips * _Point * 10;
            double tp = closes[0] - InpTP_Pips * _Point * 10;
            trade.Sell(InpLotSize, _Symbol, closes[0], sl, tp);
         }
      }
   }

   // 7. Trailing Stop
   for(int i=PositionsTotal()-1; i>=0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket)) {
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
            if(closes[0] - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point * 10) {
               double newSL = closes[0] - InpSL_Pips * _Point * 10;
               if(newSL > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         } else {
            if(PositionGetDouble(POSITION_PRICE_OPEN) - closes[0] > InpTrailStart * _Point * 10) {
               double newSL = closes[0] + InpSL_Pips * _Point * 10;
               if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}