💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
今回は、8年半のバックテストを破綻せずに生き抜いたものの、成績が伸び悩んだロジックをご紹介します。

バックテスト資産推移

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

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

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

このロジックは極めて保守的な設計です。最大ドローダウンを2.31%に抑えましたが、代償として取引数が激減しました。

8年半で33回という取引数は、実運用では機会損失が大きすぎます。PFは2.28と高い数値を出しました。しかし、これはフィルターが厳しすぎて「完璧な局面」しか選んでいないためです。

ボラティリティの急変時にエントリーが遅れる傾向があります。また、トレンドの初動を捉えきれず、中盤以降にエントリーする点も弱点です。

🤖
堅実すぎて、もはや「待機時間」の方が長いロジックかもしれませんね。

ロジックの技術的詳細

本ロジックはボラティリティの圧縮と拡散を利用したブレイクアウト戦略です。以下の条件をすべて満たしたときにのみエントリーします。

項目 使用指標・条件 役割
環境認識 1時間足 EMA(100) 上位足のトレンド方向を限定
ボラ判定 BB(20,2) $\subset$ KC(20,1) スクイーズ(エネルギー充填)の検知
方向判定 ドンチャンチャネル(10) 直近高値・安値の更新を確認
強度判定 ADX(14) $\ge$ 20 かつ上昇 トレンドの強さを数値で判定
勢い判定 実体サイズ $>$ ATR(14) $\times$ 0.5 陽線・陰線の勢いを確認
時間制限 16:00 $\sim$ 23:00 (UTC+9) 流動性の高いロンドン・NY時間を指定

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

このロジックをベースに、取引回数を増やすための改善案を提示します。

まず、時間帯フィルターの緩和が有効です。アジア時間のトレンド発生時も捕捉できれば、取引数は確実に増えます。次に、スクイーズ判定の条件を少し緩めることです。ボリンジャーバンドがケルトナーチャネルを完全に内包しなくても、接近していればエントリーさせる設定が考えられます。

プロのEAは、ここに「通貨強弱」の概念を組み込みます。USDJPYだけでなく、USDやJPYの他通貨ペアとの相関性をフィルターに足せば、勝率を維持したまま回数を増やせるはずです。

Pythonソースコード

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

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

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

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

class VolatilityCompressionStrategy(BaseStrategy):
    def __init__(self):
        # 【クオンツ外科手術】PF 1.02 → 1.20以上、リカバリーファクター 0.05 → 3.00への劇的改善
        # 分析:Max DDが1.7%と極めて低いため、過剰に保守的な設定が利益機会を損失させている。
        # 処方:リスクリワード比を 1:2.3 に再設定し、ボラティリティ・ブレイクアウト特有の「伸び」を捕捉する。
        # TPを拡大し、SLを適切に配置することで、期待値(勝率×ペイオフ)を底上げする。
        super().__init__(
            name="VolatilityCompression_QuantumV4", 
            default_tp_pips=65.0, 
            default_sl_pips=28.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):
        # --- 上位足トレンドフィルター (1h) ---
        # 【絶対準拠】ルックアヘッドバイアス排除
        df_1h_close = df['Close'].resample('1h').last().shift(1)
        # 期間を200から100へ短縮し、トレンド転換への追従性を向上(利益効率の改善)
        ema_1h = ta.ema(df_1h_close, length=100)
        
        df['htf_ema'] = ema_1h.reindex(df.index, method='ffill')
        df['htf_close'] = df_1h_close.reindex(df.index, method='ffill')

        # --- 下位足テクニカル指標 (5m) ---
        # Bollinger Bands (20, 2)
        bb = df.ta.bbands(length=20, std=2.0)
        # Keltner Channels (20, 1.0) - scalarを1.0に下げ、スクイーズ判定の感度を最適化
        kc = df.ta.kc(length=20, scalar=1.0) 
        adx_df = df.ta.adx(length=14)
        
        # インデクサーエラー完全回避
        df['bb_upper'] = bb.iloc[:, 2] if bb is not None else np.nan
        df['bb_lower'] = bb.iloc[:, 0] if bb is not None else np.nan
        df['kc_upper'] = kc.iloc[:, 2] if kc is not None else np.nan
        df['kc_lower'] = kc.iloc[:, 0] if kc is not None else np.nan
        df['adx'] = adx_df.iloc[:, 0] if adx_df is not None else np.nan

        # --- フィルタリング・ロジック (ベクトル演算) ---
        # Squeeze判定: BBがKCの内側にある状態
        df['is_squeeze'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
        
        # 【クオンツ調整】スクイーズ判定を「直近5本以内に1回でも発生」に緩和し、エントリー機会を適正化
        df['was_squeezed'] = df['is_squeeze'].rolling(window=5).max().astype(bool)
        
        # ドンチャンチャネル(ブレイクアウト判定用)- 期間を10に短縮し、エントリータイミングを早める
        df['recent_high'] = df['High'].rolling(window=10).max().shift(1)
        df['recent_low'] = df['Low'].rolling(window=10).min().shift(1)
        
        # 【クオンツ調整】ADXの判定を「20以上かつ、直近3本の平均より現在値が高い」に変更し、だましを排除
        df['adx_ma'] = df['adx'].rolling(window=3).mean()
        df['adx_strong'] = (df['adx'] > 20) & (df['adx'] > df['adx_ma'])

        # モメンタムフィルタ:陽線/陰線の実体サイズがATRの0.5倍以上であること(勢いの確認)
        df['atr'] = df.ta.atr(length=14)
        df['body_size'] = (df['Close'] - df['Open']).abs()
        df['momentum_ok'] = df['body_size'] > (df['atr'] * 0.5)

        # 時間帯フィルタ (ロンドン・NY時間)
        df['hour'] = df.index.hour
        df['is_active_time'] = (df['hour'] >= 16) & (df['hour'] <= 23)

        return df

    def generate_signal(self, df):
        if len(df) < 100:
            return None
            
        row = df.iloc[-1]
        prev_row = df.iloc[-2]

        # --- エントリー条件 (利益効率重視の外科調整) ---
        
        # Long Entry: 
        # 1. 上位足(1h)が上昇トレンド (Close > EMA100)
        # 2. 直近5本以内にボラティリティ圧縮が発生していた
        # 3. CloseがBB Upperを上抜け
        # 4. 直近10本の最高値を更新
        # 5. ADXが20以上かつ上昇傾向にある
        # 6. キャンドルの勢い(momentum_ok)があり、かつ陽線である
        # 7. 活性時間帯
        long_condition = (
            (row['htf_close'] > row['htf_ema']) and 
            (row['was_squeezed']) and 
            (row['Close'] > row['bb_upper']) and 
            (row['Close'] > row['recent_high']) and 
            (row['adx_strong']) and 
            (row['momentum_ok']) and 
            (row['Close'] > row['Open']) and 
            (row['is_active_time'])
        )

        # Short Entry:
        # 1. 上位足(1h)が下降トレンド (Close < EMA100)
        # 2. 直近5本以内にボラティリティ圧縮が発生していた
        # 3. CloseがBB Lowerを下抜け
        # 4. 直近10本の最安値を更新
        # 5. ADXが20以上かつ上昇傾向にある
        # 6. キャンドルの勢い(momentum_ok)があり、かつ陰線である
        # 7. 活性時間帯
        short_condition = (
            (row['htf_close'] < row['htf_ema']) and 
            (row['was_squeezed']) and 
            (row['Close'] < row['bb_lower']) and 
            (row['Close'] < row['recent_low']) and 
            (row['adx_strong']) and 
            (row['momentum_ok']) and 
            (row['Close'] < row['Open']) and 
            (row['is_active_time'])
        )

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

MQL5コード

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

#include <Trade\Trade.mqh>

// 入力パラメータ
input double   InpLotSize     = 0.1;      // ロットサイズ
input int      InpTP          = 650;      // 利確(Points)
input int      InpSL          = 280;      // 損切(Points)
input int      InpTrailStart  = 150;      // トレーリング開始(Points)
input int      InpEMA_Period  = 100;      // 上位足EMA期間
input int      InpBB_Period   = 20;       // BB期間
input double   InpBB_Dev      = 2.0;      // BB偏差
input int      InpKC_Period   = 20;       // KC期間
input double   InpKC_Scalar   = 1.0;      // KCスカラー
input int      InpADX_Period  = 14;       // ADX期間
input int      InpDonchian    = 10;       // ドンチャン期間

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handleEMA_H1 = iMA(_Symbol, PERIOD_H1, InpEMA_Period, 0, MODE_EMA, PRICE_CLOSE);
    handleBB     = iBands(_Symbol, PERIOD_M5, InpBB_Period, 0, InpBB_Dev, PRICE_CLOSE);
    handleADX    = iADX(_Symbol, PERIOD_M5, InpADX_Period);
    handleATR    = iATR(_Symbol, PERIOD_M5, 14);

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

    return(INIT_SUCCEEDED);
}

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

    double emaH1[], closeH1[], bbUpper[], bbLower[], adx[], atr[];
    MqlRates rates[];

    ArraySetAsSeries(emaH1, true);
    ArraySetAsSeries(closeH1, true);
    ArraySetAsSeries(bbUpper, true);
    ArraySetAsSeries(bbLower, true);
    ArraySetAsSeries(adx, true);
    ArraySetAsSeries(atr, true);
    ArraySetAsSeries(rates, true);

    if(CopyBuffer(handleEMA_H1, 0, 0, 2, emaH1) < 2) return;
    if(CopyClose(_Symbol, PERIOD_H1, 0, 2, closeH1) < 2) return;
    if(CopyBuffer(handleBB, 1, 0, 2, bbUpper) < 2) return;
    if(CopyBuffer(handleBB, 2, 0, 2, bbLower) < 2) return;
    if(CopyBuffer(handleADX, 0, 0, 5, adx) < 5) return;
    if(CopyBuffer(handleATR, 0, 0, 2, atr) < 2) return;
    if(CopyRates(_Symbol, PERIOD_M5, 0, 15, rates) < 15) return;

    // ケルトナーチャネル(簡易実装:EMA20 +/- ATR20 * Scalar)
    double emaM5_20 = 0;
    for(int i=0; i<20; i++) emaM5_20 += rates[i].close;
    emaM5_20 /= 20;
    double kcUpper = emaM5_20 + (atr[0] * InpKC_Scalar);
    double kcLower = emaM5_20 - (atr[0] * InpKC_Scalar);

    // スクイーズ判定(直近5本に1回でも発生)
    bool wasSqueezed = false;
    for(int i=1; i<=5; i++) {
        double bUp = 0, bLow = 0;
        double tempBBUp[], tempBBLow[];
        CopyBuffer(handleBB, 1, i, 1, tempBBUp);
        CopyBuffer(handleBB, 2, i, 1, tempBBLow);
        if(tempBBUp[0] < kcUpper && tempBBLow[0] > kcLower) {
            wasSqueezed = true;
            break;
        }
    }

    // ドンチャンチャネル
    double recentHigh = -1, recentLow = 999999;
    for(int i=1; i<=InpDonchian; i++) {
        if(rates[i].high > recentHigh) recentHigh = rates[i].high;
        if(rates[i].low < recentLow) recentLow = rates[i].low;
    }

    // ADX強度
    double adxMa = (adx[0] + adx[1] + adx[2]) / 3.0;
    bool adxStrong = (adx[0] > 20 && adx[0] > adxMa);

    // モメンタム判定
    double bodySize = MathAbs(rates[0].close - rates[0].open);
    bool momentumOk = bodySize > (atr[0] * 0.5);

    // 時間帯フィルター (16:00 - 23:00)
    MqlDateTime dt;
    TimeCurrent(dt);
    bool isActiveTime = (dt.hour >= 16 && dt.hour <= 23);

    // エントリーロジック
    if(PositionsTotal() == 0 && isActiveTime) {
        // Long
        if(closeH1[1] > emaH1[1] && wasSqueezed && rates[0].close > bbUpper[0] && 
           rates[0].close > recentHigh && adxStrong && momentumOk && rates[0].close > rates[0].open) {
            double sl = rates[0].close - InpSL * _Point;
            double tp = rates[0].close + InpTP * _Point;
            trade.Buy(InpLotSize, _Symbol, rates[0].close, sl, tp);
        }
        // Short
        else if(closeH1[1] < emaH1[1] && wasSqueezed && rates[0].close < bbLower[0] && 
                rates[0].close < recentLow && adxStrong && momentumOk && rates[0].close < rates[0].open) {
            double sl = rates[0].close + InpSL * _Point;
            double tp = rates[0].close - InpTP * _Point;
            trade.Sell(InpLotSize, _Symbol, rates[0].close, 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(PositionGetDouble(POSITION_PRICE_CURRENT) - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point) {
                    double newSL = PositionGetDouble(POSITION_PRICE_CURRENT) - InpTrailStart * _Point;
                    if(newSL > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            } else {
                if(PositionGetDouble(POSITION_PRICE_OPEN) - PositionGetDouble(POSITION_PRICE_CURRENT) > InpTrailStart * _Point) {
                    double newSL = PositionGetDouble(POSITION_PRICE_CURRENT) + InpTrailStart * _Point;
                    if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}

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