💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は、本当に孤独で険しい道のりです。多くの人が「聖杯」を追い求め、カーブフィッティングの罠に落ちて資金を溶かします。今回は、10年という全期間のバックテストを破綻せずに生き抜いたロジックを紹介します。ただし、成績は決して派手ではありません。むしろ、伸び悩んだと言える結果です。

もともと私は、ブレイクアウト手法の「だまし」に絶望していました。ボリンジャーバンドを抜けた瞬間に反転する。そんな経験を何度も繰り返しました。そこで、「ボラティリティが極限まで凝縮された状態」を定義し、エネルギーが爆発した瞬間だけを狙う戦略を考えました。試行錯誤の末にたどり着いたのが、このスクイーズ戦略です。

🤖
派手な利益を狙うのではなく、まずは「生き残ること」だけに集中して作りました。

このコードを手に入れることで、あなたが得られるメリットは明確です。MT5のEA開発に必要な基礎構造を、ゼロから構築する数百時間を節約できます。長期的に破綻しないためのフィルター設計は、非常に価値のある土台となるはずです。自分だけのオリジナルEAを作るための「研究素材」として活用してください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 1.25
勝率 53.9%
総取引数 102 回
純利益 (0.1ロット/1万通貨時) ¥16644.48
最大ドローダウン 1.88%
リカバリーファクター 0.88
期待利得 (Expected Payoff) 0.57

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

このロジックの最大の弱点は、取引回数の少なさと収益性の低さです。10年で102回という回数は、年平均に直すとわずか10回程度になります。チャンスを待ちすぎるあまり、多くの利益機会を逃しています。

PF 1.25という数字も、プロの視点から見れば平凡な成績です。これは、カーブフィッティングを徹底的に排除し、汎用性を重視した結果といえます。また、トレンドが緩やかな相場では全く機能せず、機会損失が激しい点も明確な課題です。

ロジックの技術的詳細

本ロジックは、マルチタイムフレーム分析とボラティリティの凝縮を組み合わせた設計です。

要素 設定・条件 役割
長期トレンド 1時間足 EMA 200 全体的な方向性の決定
中期トレンド 15分足 EMA 50 + 傾き判定 トレンドの勢いを確認
凝縮判定 BB(20, 2.2) $\subset$ KC(20, 1.2) ボラティリティのスクイーズを検知
勢い判定 ADX > 20 + RSI (50-70 / 30-50) ブレイクアウトの正当性を確認
時間フィルター 14時 $\sim$ 翌4時 流動性の高い時間帯に限定
出口戦略 TP: 50pips / SL: 15pips リスクリワード 3.33:1 の追求
ポイントは「BBがケルトナーチャンネルの内側に入り込む」状態でエネルギーを溜め、そこからBBを突き抜けた瞬間を狙う点にあります。

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

このロジックを「化けさせる」には、環境認識の精度を上げることです。現在は単純なEMAの傾きで判定していますが、ここに上位足のサポレジラインの概念を加えれば、勝率はさらに向上するでしょう。

また、固定の利確・損切りではなく、ATRに基づいたダイナミックな決済ロジックへの変更を推奨します。相場のボラティリティに合わせて幅を変えることで、期待利得の底上げが期待できます。この無料コードをベースに、ぜひあなた独自のフィルターを組み合わせてみてください。

Pythonコードの公開

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

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

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

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

class VSMBStrategy(BaseStrategy):
    def __init__(self):
        super().__init__(
            name="Volatility Squeeze & Momentum Breakout",
            default_tp_pips=50.0,      # PF向上と勝率確保のため、現実的な利確目標へ調整
            default_sl_pips=15.0,      # リスクリワード 3.33:1 を維持し、リカバリーファクターを追求
            enable_trailing_stop=True,
            trail_start_pips=15.0      # 利益確定の確実性を高め、ドローダウンを抑制
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15m", "1h"]

    def calculate_indicators(self, df):
        # --- MTF Trend & Slope Filter ---
        # 1h EMA 200 (長期トレンド)
        h1_close = df['Close'].resample('1h').last().shift(1)
        h1_ema = ta.ema(h1_close, length=200)
        df['htf_ema200'] = h1_ema.reindex(df.index, method='ffill')
        
        # 15m EMA 50 (中期トレンド + 傾き判定)
        m15_close = df['Close'].resample('15min').last().shift(1)
        m15_ema = ta.ema(m15_close, length=50)
        df['mtf_ema50'] = m15_ema.reindex(df.index, method='ffill')
        # 傾きを判定し、トレンドの「勢い」を確認してだましを排除
        df['mtf_ema_slope_bull'] = df['mtf_ema50'] > df['mtf_ema50'].shift(3)
        df['mtf_ema_slope_bear'] = df['mtf_ema50'] < df['mtf_ema50'].shift(3)

        df['htf_trend_bull'] = (df['Close'] > df['htf_ema200']) & (df['Close'] > df['mtf_ema50']) & df['mtf_ema_slope_bull']
        df['htf_trend_bear'] = (df['Close'] < df['htf_ema200']) & (df['Close'] < df['mtf_ema50']) & df['mtf_ema_slope_bear']

        # --- Volatility Indicators (5m) ---
        # BB期間を20に回帰させつつ、標準偏差を2.2へ引き上げ、より精度の高いブレイクアウトを検知
        bb = ta.bbands(df['Close'], length=20, std=2.2)
        df['bb_lower'] = bb.iloc[:, 0] 
        df['bb_upper'] = bb.iloc[:, 2] 

        # Keltner Channels (スカラーを1.2へタイト化し、より「凝縮された」スクイーズを定義)
        kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=1.2)
        df['kc_lower'] = kc.iloc[:, 0] 
        df['kc_upper'] = kc.iloc[:, 2] 

        # ATR & Volatility Baseline
        df['atr'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        df['atr_sma'] = df['atr'].rolling(window=60).mean()
        df['volatility_ok'] = df['atr'] / df['atr_sma'] > 0.8

        # ADX (Trend Strength)
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['adx'] = adx_df.iloc[:, 0] 

        # --- Momentum Filter (RSI) ---
        # RSIを単なる上限ではなく、トレンド方向への「勢い」の証明として利用
        df['rsi'] = ta.rsi(df['Close'], length=14)

        # --- Squeeze Logic ---
        df['is_squeezed'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
        # 凝縮期間を3本に設定し、エネルギー充填を確認
        df['squeeze_confirmed'] = df['is_squeezed'].rolling(window=3).sum() >= 3

        # --- Time Filter ---
        df['hour'] = df.index.hour
        df['time_ok'] = (df['hour'] >= 14) | (df['hour'] <= 4)

        return df

    def generate_signal(self, df):
        if len(df) < 200:
            return None

        row = df.iloc[-1]
        prev_row = df.iloc[-2]

        # Common conditions
        is_volatility_ok = row['volatility_ok']
        is_trend_strong = row['adx'] > 20 # 20に緩和しつつ、EMA傾きで質を担保
        is_time_ok = row['time_ok']
        was_squeezed = prev_row['squeeze_confirmed']

        # BREAKOUT Quality Check: ブレイクアウト時の足が平均的なボラティリティを超えているか
        # (Volume不使用ルールの代替として、ローソク足のサイズで勢いを判定)
        candle_range = row['High'] - row['Low']
        is_explosive = candle_range > (row['atr'] * 0.8)

        # Long Entry: HTF Bull + Slope OK + Vol OK + ADX OK + Time OK + Prev Squeeze + BB Breakout + RSI Momentum
        if (row['htf_trend_bull'] and is_volatility_ok and is_trend_strong and 
            is_time_ok and was_squeezed and is_explosive and 
            row['Close'] > row['bb_upper'] and 
            row['Close'] > row.get('Open', 0) and 
            50 < row['rsi'] < 70): # RSI 50-70の「強い上昇トレンド」圏内
            return 'BUY'

        # Short Entry: HTF Bear + Slope OK + Vol OK + ADX OK + Time OK + Prev Squeeze + BB Breakout + RSI Momentum
        if (row['htf_trend_bear'] and is_volatility_ok and is_trend_strong and 
            is_time_ok and was_squeezed and is_explosive and 
            row['Close'] < row['bb_lower'] and 
            row['Close'] < row.get('Open', float('inf')) and 
            30 < row['rsi'] < 50): # RSI 30-50の「強い下落トレンド」圏内
            return 'SELL'

        return None

MQL5コードへの翻訳

//+------------------------------------------------------------------+
//|                                    VolatilitySqueezeMomentum.mq5 |
//|                                  Copyright 2026, EA Research Lab |
//|                                             https://example.com  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, EA Research Lab"
#property link      "https://example.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input Parameters
input double   InpLotSize      = 0.1;      // Lot Size
input int      InpTP           = 500;      // Take Profit (points)
input int      InpSL           = 150;      // Stop Loss (points)
input int      InpTrailingStart = 150;     // Trailing Start (points)
input int      InpBBPeriod     = 20;       // BB Period
input double   InpBBStdDev     = 2.2;      // BB StdDev
input int      InpKCPeriod     = 20;       // KC Period
input double   InpKCScalar     = 1.2;      // KC Scalar
input int      InpEMA_Long     = 200;      // H1 EMA Period
input int      InpEMA_Mid      = 50;       // M15 EMA Period

//--- Global Handles
int hBB, hKC, hATR, hADX, hRSI, hEMA_H1, hEMA_M15;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   hBB    = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
   hKC    = iCustom(_Symbol, PERIOD_M5, "KeltnerChannel", InpKCPeriod, InpKCScalar); // Assuming custom KC indicator
   hATR   = iATR(_Symbol, PERIOD_M5, 14);
   hADX   = iADX(_Symbol, PERIOD_M5, 14);
   hRSI   = iRSI(_Symbol, PERIOD_M5, 14, PRICE_CLOSE);
   hEMA_H1  = iMA(_Symbol, PERIOD_H1, InpEMA_Long, 0, MODE_EMA, PRICE_CLOSE);
   hEMA_M15 = iMA(_Symbol, PERIOD_M15, InpEMA_Mid, 0, MODE_EMA, PRICE_CLOSE);

   if(hBB == INVALID_HANDLE || hEMA_H1 == INVALID_HANDLE || hEMA_M15 == INVALID_HANDLE) return(INIT_FAILED);
   return(INIT_SUCCEEDED);
}

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

   double close[], open[], high[], low[];
   CopyClose(_Symbol, PERIOD_M5, 0, 3, close);
   CopyOpen(_Symbol, PERIOD_M5, 0, 3, open);
   CopyHigh(_Symbol, PERIOD_M5, 0, 3, high);
   CopyLow(_Symbol, PERIOD_M5, 0, 3, low);

   double bb_up[], bb_low[], kc_up[], kc_low[], atr[], adx[], rsi[];
   CopyBuffer(hBB, 1, 0, 3, bb_up);
   CopyBuffer(hBB, 2, 0, 3, bb_low);
   // Simplified KC retrieval (assuming typical buffer indices)
   CopyBuffer(hKC, 1, 0, 3, kc_up);
   CopyBuffer(hKC, 2, 0, 3, kc_low);
   CopyBuffer(hATR, 0, 0, 60, atr);
   CopyBuffer(hADX, 0, 0, 3, adx);
   CopyBuffer(hRSI, 0, 0, 3, rsi);

   double ema_h1[], ema_m15[];
   CopyBuffer(hEMA_H1, 0, 0, 1, ema_h1);
   CopyBuffer(hEMA_M15, 0, 0, 4, ema_m15);

   //--- Technical Logic
   bool mtf_slope_bull = ema_m15[0] > ema_m15[3];
   bool mtf_slope_bear = ema_m15[0] < ema_m15[3];
   bool htf_trend_bull = (close[0] > ema_h1[0] && close[0] > ema_m15[0] && mtf_slope_bull);
   bool htf_trend_bear = (close[0] < ema_h1[0] && close[0] < ema_m15[0] && mtf_slope_bear);

   bool is_squeezed = (bb_up[0] < kc_up[0] && bb_low[0] > kc_low[0]);
   bool was_squeezed = (bb_up[1] < kc_up[1] && bb_low[1] > kc_low[1]); // Simplified to 1 bar for example

   double atr_sma = 0;
   for(int i=0; i<60; i++) atr_sma += atr[i];
   atr_sma /= 60;
   bool volatility_ok = (atr[0] / atr_sma) > 0.8;
   bool is_explosive = (high[0] - low[0]) > (atr[0] * 0.8);

   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   bool time_ok = (dt.hour >= 14 || dt.hour <= 4);

   //--- Entry Signal
   if(htf_trend_bull && volatility_ok && adx[0] > 20 && time_ok && was_squeezed && is_explosive && close[0] > bb_up[0] && close[0] > open[0] && rsi[0] > 50 && rsi[0] < 70)
   {
      double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point;
      double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point;
      trade.Buy(InpLotSize, _Symbol, 0, sl, tp);
   }
   else if(htf_trend_bear && volatility_ok && adx[0] > 20 && time_ok && was_squeezed && is_explosive && close[0] < bb_low[0] && close[0] < open[0] && rsi[0] > 30 && rsi[0] < 50)
   {
      double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point;
      double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point;
      trade.Sell(InpLotSize, _Symbol, 0, sl, tp);
   }

   ManageTrailingStop();
}

void ManageTrailingStop()
{
   for(int i=PositionsTotal()-1; i>=0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
      {
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            if(SymbolInfoDouble(_Symbol, SYMBOL_BID) - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailingStart * _Point)
            {
               double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTrailingStart * _Point;
               if(new_sl > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
            }
         }
         else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
         {
            if(PositionGetDouble(POSITION_PRICE_OPEN) - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > InpTrailingStart * _Point)
            {
               double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpTrailingStart * _Point;
               if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}

bool IsNewBar()
{
   static datetime last_time = 0;
   datetime curr_time = iTime(_Symbol, PERIOD_M5, 0);
   if(last_time != curr_time) { last_time = curr_time; return true; }
   return false;
}