💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
EA開発は孤独で険しい道です。多くのロジックが数ヶ月の運用で破綻します。既存のシステムに失望した方も多いはず。今回は10年のバックテストを破綻せず生き抜いたロジックを共有します。成績は決して爆発的ではありません。しかし、生存能力だけは極めて高い研究素材です。

もともと私はブレイクアウト手法の「だまし」に悩んでいました。指標発表時の乱高下で資金を溶かした経験があります。そこで「エネルギーが蓄積された状態」を数値化したいと考えました。ボリンジャーバンドとケルトナーチャネルの相関関係に着目し、試行錯誤を繰り返しました。開発過程では何度もパラメータ調整に迷い、心折れそうになった記憶があります。

🤖
派手な利益を追うと、たいてい破綻します。今回は「生き残ること」に特化させてみました。

このコードを入手すれば、MT5のEA開発にかかる数百時間を節約できます。ゼロからロジックを組む苦労はありません。堅牢な土台があるため、自分好みのフィルターを追加して改造することが可能です。オリジナルEAを完成させるための最高のベースキャンプになります。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 1.51
勝率 52.9%
総取引数 121 回
純利益 (0.1ロット/1万通貨時) ¥49641.89
最大ドローダウン 2.36%
リカバリーファクター 2.10
期待利得 (Expected Payoff) 0.71

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

このロジックは安定していますが、収益性は低いです。10年で121回という取引数は少なすぎます。年平均に直すと、月に1回程度しかエントリーしません。これは、非常に厳しいフィルターを重ねた結果です。

また、レンジ相場での微益を積み上げる力に欠けます。トレンドが出なければ、ただ時間を消費するだけです。PF 1.51という数字は合格点ですが、資産を爆発的に増やす力はありません。保守的すぎる設計が、機会損失を招いていると言えます。

ロジックの技術的詳細

本ロジックは、低ボラティリティからの解放を狙う戦略です。以下の条件をすべて満たしたときのみエントリーします。

項目 条件・設定値 役割
上位足トレンド 1時間足 EMA(200, 50, 20) の並び 環境認識(方向性の固定)
スクイーズ判定 BB(20, 2.0) $\subset$ KC(20, 1.2) エネルギー蓄積の検知
ブレイクアウト BB上限/下限 および 20期間高値/安値の突破 モメンタムの発生確認
トレンド強度 ADX(14) > 20 トレンド発生の確信度向上
リスク管理 SL 20pips / TP 60pips リスクリワード 1:3 の固定
ポイント: 単なるブレイクアウトではなく、「スクイーズ(凝縮)」からの「エクスパンション(拡張)」を捉えている点が、最大ドローダウンを抑えている要因です。

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

このロジックをベースに、取引回数を増やす工夫を検討してください。例えば、ADXの閾値を20から15へ下げることで、エントリー機会が増える可能性があります。

また、固定pipsの利確ではなく、ATRを用いた可変利確を導入すべきです。相場のボラティリティに合わせて利確幅を変えれば、収益性は向上します。プロの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
import numpy as np

class VSMBStrategy(BaseStrategy):
    """
    Volatility Squeeze Momentum Breakout (VSMB) Strategy - Quant Optimized
    - Optimization Target: Maximize PF, Recovery Factor, and Trade Count while minimizing MaxDD.
    - Logic: BB inside KC (Squeeze) -> Breakout (Expansion) -> Trend Confirmation (H1 EMA & ADX).
    - Fix: Resolved KeyError by using positional indexing for pandas_ta outputs.
    """
    def __init__(self):
        # 【Quant Tuning】
        # TP/SLを固定pipsからボラティリティ適応的に設定するためのベース値。
        # リカバリーファクター向上のため、リスクリワード比を 1:3.0 に最適化。
        super().__init__(
            name="VSMB_Strategy_Optimized", 
            default_tp_pips=60.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):
        # --- 1. 上位足(1時間足)トレンドフィルター ---
        # ルックアヘッドバイアス排除のため、resample後の計算にshift(1)を適用
        h1_resampled = df[['Close', 'High', 'Low']].resample('1h').last()
        
        # EMA 200: 長期トレンド, EMA 50: 中期トレンド, EMA 20: 短期トレンド
        h1_ema200 = ta.ema(h1_resampled['Close'], length=200)
        h1_ema50 = ta.ema(h1_resampled['Close'], length=50)
        h1_ema20 = ta.ema(h1_resampled['Close'], length=20)
        
        # トレンド判定の最適化: EMAのパーフェクトオーダーに近い状態を定義
        h1_trend = pd.Series(0, index=h1_resampled.index)
        h1_trend.loc[(h1_resampled['Close'] > h1_ema200) & (h1_ema20 > h1_ema50)] = 1
        h1_trend.loc[(h1_resampled['Close'] < h1_ema200) & (h1_ema20 < h1_ema50)] = -1
        
        # 執行足(5m)へマージ
        df['h1_trend'] = h1_trend.shift(1).reindex(df.index, method='ffill')

        # --- 2. 執行足 (5m) インジケーター計算 ---
        # 【KeyError対策】カラム名を直接指定せず、ilocによる位置指定または動的取得を行う
        
        # ボリンジャーバンド (20, 2.0)
        bb = ta.bbands(df['Close'], length=20, std=2.0)
        # pandas_taのbbandsは [BBL, BBM, BBU] の順で出力される
        df['bb_lower'] = bb.iloc[:, 0]
        df['bb_upper'] = bb.iloc[:, 2]

        # ケルトナーチャネル (20, 1.2) 
        # 【Tuning】scalarを1.5->1.2に下方修正し、スクイーズ判定の感度を向上させ取引数を確保
        kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=1.2)
        # pandas_taのkcは [KCL, KC_Mid, KCU] の順で出力される
        df['kc_lower'] = kc.iloc[:, 0]
        df['kc_upper'] = kc.iloc[:, 2]

        # ADX (14)
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['adx'] = adx_df['ADX_14']

        # ATR (14) - リスク管理用(Volumeの代替)
        df['atr'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)

        # --- 3. 戦略的判定用データ ---
        # Squeeze判定: BBがKCの内部にあるか (低ボラティリティ局面の特定)
        df['is_squeeze'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
        
        # ドンチアン的な高値・安値ブレイク判定 (20期間)
        # 現在足を含めないためshift(1)を徹底
        df['recent_high'] = df['High'].rolling(window=20).max().shift(1)
        df['recent_low'] = df['Low'].rolling(window=20).min().shift(1)
        
        # ADXの方向性判定: 20以上のトレンド圏で、かつ上昇しているか
        df['adx_rising'] = df['adx'] > df['adx'].shift(1)

        return df

    def generate_signal(self, df):
        # 最新の行および1本前の行データを取得
        if len(df) < 2:
            return None
            
        row = df.iloc[-1]
        prev_row = df.iloc[-2]

        # --- Long Entry Logic (Optimized) ---
        # 1. 上位足トレンドが上昇 (H1 Trend Bull)
        # 2. 1本前までスクイーズ状態であった (Energy Accumulation)
        # 3. 現在足でスクイーズを解消し、BB UpperがKC Upperを上抜ける (Volatility Breakout)
        # 4. 価格がBB Upperおよび直近高値を同時にブレイク (Strong Momentum)
        # 5. ADXがトレンド閾値(20)を超えている (Trend Confirmation)
        long_condition = (
            (row['h1_trend'] == 1) and
            (prev_row['is_squeeze'] == True) and
            (row['bb_upper'] > row['kc_upper']) and
            (row['Close'] > row['bb_upper']) and
            (row['Close'] > row['recent_high']) and
            (row['adx'] > 20)
        )

        # --- Short Entry Logic (Optimized) ---
        # 1. 上位足トレンドが下降 (H1 Trend Bear)
        # 2. 1本前までスクイーズ状態であった (Energy Accumulation)
        # 3. 現在足でスクイーズを解消し、BB LowerがKC Lowerを下抜ける (Volatility Breakout)
        # 4. 価格がBB Lowerおよび直近安値を同時にブレイク (Strong Momentum)
        # 5. ADXがトレンド閾値(20)を超えている (Trend Confirmation)
        short_condition = (
            (row['h1_trend'] == -1) and
            (prev_row['is_squeeze'] == True) and
            (row['bb_lower'] < row['kc_lower']) and
            (row['Close'] < row['bb_lower']) and
            (row['Close'] < row['recent_low']) and
            (row['adx'] > 20)
        )

        if long_condition:
            return 'BUY'
        elif short_condition:
            return 'SELL'
        
        return None

MQL5コード(MT5用)

//+------------------------------------------------------------------+
//|                                              VSMB_Optimized.mq5 |
//|                                  Copyright 2026, AI Researcher   |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input parameters
input int      InpPeriod      = 20;       // Period for BB and KC
input double   InpBBStdDev    = 2.0;      // Bollinger Bands StdDev
input double   InpKCScalar    = 1.2;      // Keltner Channel Scalar
input int      InpADXPeriod   = 14;       // ADX Period
input int      InpADXMin      = 20;       // Minimum ADX value
input double   InpTP          = 600;      // Take Profit in points
input double   InpSL          = 200;      // Stop Loss in points
input double   InpTrailStart  = 150;      // Trailing Start in points
input double   InpLotSize     = 0.1;      // Trade Lot Size

//--- Global variables
CTrade         trade;
int            handle_h1_ema200, handle_h1_ema50, handle_h1_ema20;
int            handle_bb, handle_adx, handle_atr;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // H1 Timeframe Indicators
    handle_h1_ema200 = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
    handle_h1_ema50  = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
    handle_h1_ema20  = iMA(_Symbol, PERIOD_H1, 20, 0, MODE_EMA, PRICE_CLOSE);
    
    // M5 Timeframe Indicators
    handle_bb  = iBands(_Symbol, PERIOD_M5, InpPeriod, 0, InpBBStdDev, PRICE_CLOSE);
    handle_adx = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
    handle_atr = iATR(_Symbol, PERIOD_M5, InpADXPeriod);
    
    if(handle_h1_ema200 == INVALID_HANDLE || handle_bb == INVALID_HANDLE || handle_adx == INVALID_HANDLE)
        return(INIT_FAILED);
        
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // Check for new bar on M5
    static datetime last_bar = 0;
    datetime current_bar = iTime(_Symbol, PERIOD_M5, 0);
    if(last_bar == current_bar) return;
    last_bar = current_bar;

    // Data arrays
    double ema200[], ema50[], ema20[], close_h1[];
    double bb_upper[], bb_lower[], adx[], close_m5[], high_m5[], low_m5[];
    
    ArraySetAsSeries(ema200, true);
    ArraySetAsSeries(ema50, true);
    ArraySetAsSeries(ema20, true);
    ArraySetAsSeries(close_h1, true);
    ArraySetAsSeries(bb_upper, true);
    ArraySetAsSeries(bb_lower, true);
    ArraySetAsSeries(adx, true);
    ArraySetAsSeries(close_m5, true);
    ArraySetAsSeries(high_m5, true);
    ArraySetAsSeries(low_m5, true);

    if(CopyBuffer(handle_h1_ema200, 0, 1, 1, ema200) <= 0) return;
    if(CopyBuffer(handle_h1_ema50, 0, 1, 1, ema50) <= 0) return;
    if(CopyBuffer(handle_h1_ema20, 0, 1, 1, ema20) <= 0) return;
    if(CopyClose(_Symbol, PERIOD_H1, 1, 1, close_h1) <= 0) return;
    if(CopyBuffer(handle_bb, 1, 0, 2, bb_upper) <= 0) return;
    if(CopyBuffer(handle_bb, 2, 0, 2, bb_lower) <= 0) return;
    if(CopyBuffer(handle_adx, 0, 0, 2, adx) <= 0) return;
    if(CopyClose(_Symbol, PERIOD_M5, 0, 2, close_m5) <= 0) return;
    if(CopyHigh(_Symbol, PERIOD_M5, 0, 21, high_m5) <= 0) return;
    if(CopyLow(_Symbol, PERIOD_M5, 0, 21, low_m5) <= 0) return;

    // Keltner Channel calculation (Manual)
    double atr_buffer[];
    ArraySetAsSeries(atr_buffer, true);
    CopyBuffer(handle_atr, 0, 0, 2, atr_buffer);
    
    double ema_m5[];
    int handle_ema_m5 = iMA(_Symbol, PERIOD_M5, InpPeriod, 0, MODE_EMA, PRICE_CLOSE);
    ArraySetAsSeries(ema_m5, true);
    CopyBuffer(handle_ema_m5, 0, 0, 2, ema_m5);

    double kc_upper_curr = ema_m5[0] + (InpKCScalar * atr_buffer[0]);
    double kc_lower_curr = ema_m5[0] - (InpKCScalar * atr_buffer[0]);
    double kc_upper_prev = ema_m5[1] + (InpKCScalar * atr_buffer[1]);
    double kc_lower_prev = ema_m5[1] - (InpKCScalar * atr_buffer[1]);

    // Squeeze Detection
    bool is_squeeze_prev = (bb_upper[1] < kc_upper_prev) && (bb_lower[1] > kc_lower_prev);
    
    // Recent High/Low (Donchian 20)
    double recent_high = high_m5[1];
    double recent_low = low_m5[1];
    for(int i=1; i<21; i++) {
        if(high_m5[i] > recent_high) recent_high = high_m5[i];
        if(low_m5[i] < recent_low) recent_low = low_m5[i];
    }

    // H1 Trend
    int h1_trend = 0;
    if(close_h1[0] > ema200[0] && ema20[0] > ema50[0]) h1_trend = 1;
    if(close_h1[0] < ema200[0] && ema20[0] < ema50[0]) h1_trend = -1;

    // Entry Logic
    if(PositionsTotal() == 0) 
    {
        // Long
        if(h1_trend == 1 && is_squeeze_prev && bb_upper[0] > kc_upper_curr && 
           close_m5[0] > bb_upper[0] && close_m5[0] > recent_high && adx[0] > InpADXMin) 
        {
            double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point;
            double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point;
            trade.Buy(InpLotSize, _Symbol, 0, sl, tp, "VSMB_Long");
        }
        // Short
        else if(h1_trend == -1 && is_squeeze_prev && bb_lower[0] < kc_lower_curr && 
                close_m5[0] < bb_lower[0] && close_m5[0] < recent_low && adx[0] > InpADXMin) 
        {
            double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point;
            double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point;
            trade.Sell(InpLotSize, _Symbol, 0, sl, tp, "VSMB_Short");
        }
    }
    
    // Trailing Stop
    for(int i=PositionsTotal()-1; i>=0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionSelectByTicket(ticket))
        {
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            {
                double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
                if(bid - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point)
                {
                    double new_sl = bid - InpSL * _Point;
                    if(new_sl > PositionGetDouble(POSITION_SL))
                        trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
                }
            }
            else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            {
                double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
                if(PositionGetDouble(POSITION_PRICE_OPEN) - ask > InpTrailStart * _Point)
                {
                    double new_sl = ask + InpSL * _Point;
                    if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)
                        trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}