直近2年間の激しい金利変動相場を勝ち抜いた優秀なEAロジックを公開します。

バックテスト資産推移

これが直近2年間を生き抜いた資産推移の証拠です。(※本ロジックは 5分足 専用です)

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

なぜこのロジックは生き残れたのか

結論から述べます。ボラティリティの蓄積と爆発を正確に捉えたからです。

理由は3点あります。1つ目はスクイーズ判定の導入です。ボリンジャーバンドがケルトナーチャネルの内側に収まる状態を検知し、エネルギーの蓄積を判断します。2つ目はマルチタイムフレーム分析の採用です。15分足と1時間足のトレンド方向へのみエントリーを限定しました。3つ目は時間帯フィルターの適用です。流動性が極めて高いロンドン・NY時間のみに取引を絞っています。

具体例を挙げます。欧州時間の開始直後にボラティリティが急拡大した際、トレンド方向へのブレイクアウトのみを抽出してエントリーします。これにより、レンジ相場での往復ビンタを完全に回避できました。

結論として、不要な取引を徹底的に排除した設計が、低ドローダウンでの生存を可能にしました。

🤖
取引回数が少ないのは、それだけ厳選したポイントでしか撃っていない証拠ですね。

戦略の技術的詳細

要素 設定・条件 役割
ベース時間足 5分足 エントリータイミングの決定
環境認識足 15分足(EMA10) / 1時間足(EMA50) 上位足のトレンド方向の判定
スクイーズ判定 BB(20, 2.0) $\subset$ KC(20, 2.0) 相場のエネルギー蓄積を検知
ブレイク判定 BBがKCを突き抜け、直近10本高安値を更新 ボラティリティ爆発の検知
モメンタム MACDヒストグラムの加速 $\text{and}$ ADX上昇 トレンドの勢いを確認
時間フィルター 16-18時 / 21-24時 (サーバー時間) 高流動性時間帯への限定
最大ドローダウンを1.78%という極めて低い水準に抑えつつ、純利益を積み上げている点がこのロジックの最大の強みです。

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 VolatilityBreakoutStrategy(BaseStrategy):
    def __init__(self):
        # 親クラスの初期化: 期待値を高めるため、TPを少し伸ばし、トレーリングストップで利益を確保
        super().__init__(
            name="Alpha_Squeeze_Expansion_V4",
            default_tp_pips=60.0, 
            default_sl_pips=20.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):
        """
        ボラティリティの収縮(Squeeze)と拡散(Expansion)を検知するロジック
        """
        # --- 5分足 (ベース) ---
        # ボリンジャーバンド (20, 2.0)
        bb = df.ta.bbands(length=20, std=2.0)
        df['BBL'] = bb.iloc[:, 0]
        df['BBM'] = bb.iloc[:, 1]
        df['BBU'] = bb.iloc[:, 2]

        # ケルトナーチャネル (20, 2.0) - スクイーズ判定の基準
        kc = df.ta.kc(length=20, scalar=2.0)
        df['KCL'] = kc.iloc[:, 0]
        df['KCU'] = kc.iloc[:, 2]

        # スクイーズ判定: BBがKCの内側に収まっている = エネルギー蓄積
        df['Squeeze'] = (df['BBU'] < df['KCU']) & (df['BBL'] > df['KCL'])
        
        # MACD - モメンタムの加速を判定
        macd = df.ta.macd(fast=12, slow=26, signal=9)
        df['MACD_Hist'] = macd.iloc[:, 2]

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

        # Price Action: 直近10本の最高値・最安値
        df['HH_10'] = df['High'].rolling(window=10).max().shift(1)
        df['LL_10'] = df['Low'].rolling(window=10).min().shift(1)

        # --- マルチタイムフレーム (MTF) 分析 ---
        # 未来のカンニングを完全に防ぐため、resample後に .shift(1) を適用
        
        # 15分足トレンド (EMA 10) - より短期的な方向性へ感度を上げる
        df_15m_close = df['Close'].resample('15min').last().shift(1)
        ema_15m = ta.ema(df_15m_close, length=10)
        df['EMA_15m'] = ema_15m.reindex(df.index, method='ffill')

        # 1時間足トレンド (EMA 50) - 中期的な環境認識
        df_1h_close = df['Close'].resample('1h').last().shift(1)
        ema_1h = ta.ema(df_1h_close, length=50)
        df['EMA_1h'] = ema_1h.reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        """
        スクイーズからの脱出とモメンタムの加速を捉えるエントリーロジック
        """
        if len(df) < 200:
            return None

        last = df.iloc[-1]
        prev = df.iloc[-2]

        # --- 1. エントリー時間帯フィルター (流動性のピーク時間) ---
        try:
            current_hour = df.index[-1].hour
        except AttributeError:
            return None

        # 16:00-18:00 (ロンドン) または 21:00-24:00 (NY)
        if not ((16 <= current_hour < 18) or (21 <= current_hour < 24)):
            return None

        # --- 2. 相場環境フィルター (MTF) ---
        # 上位足のトレンド方向にのみエントリー
        long_env = (last['Close'] > last['EMA_1h']) and (last['Close'] > last['EMA_15m'])
        short_env = (last['Close'] < last['EMA_1h']) and (last['Close'] < last['EMA_15m'])

        # --- 3. スクイーズ・リリース判定 (核心部分) ---
        # 前回の足でスクイーズしており、今回の足でBBがKCを突き抜けた = ボラティリティの爆発
        squeeze_release_long = prev['Squeeze'] and (last['BBU'] > last['KCU'])
        squeeze_release_short = prev['Squeeze'] and (last['BBL'] < last['KCL'])

        # --- 4. モメンタムの加速判定 ---
        # MACDヒストグラムが正方向へ加速しているか
        momentum_up = (last['MACD_Hist'] > 0) and (last['MACD_Hist'] > prev['MACD_Hist'])
        momentum_down = (last['MACD_Hist'] < 0) and (last['MACD_Hist'] < prev['MACD_Hist'])

        # --- 最終エントリー判定 ---
        # ロング条件:
        # - MTF環境が上昇トレンド
        # - スクイーズ状態からボラティリティが爆発(Release)
        # - モメンタムが正方向に加速
        # - 終値がBBUを上抜け、かつ直近高値を更新
        if long_env and squeeze_release_long and momentum_up:
            if (last['Close'] > last['BBU']) and (last['Close'] > last['HH_10']):
                # ADXが上昇傾向にあるときのみ、トレンドの持続性を期待してエントリー
                if last['ADX'] > prev['ADX']:
                    return 'BUY'

        # ショート条件:
        # - MTF環境が下降トレンド
        # - スクイーズ状態からボラティリティが爆発(Release)
        # - モメンタムが負方向に加速
        # - 終値がBBLを下抜け、かつ直近安値を更新
        if short_env and squeeze_release_short and momentum_down:
            if (last['Close'] < last['BBL']) and (last['Close'] < last['LL_10']):
                # ADXが上昇傾向にあるときのみ、トレンドの持続性を期待してエントリー
                if last['ADX'] > prev['ADX']:
                    return 'SELL'

        return None

MQL5コード

//+------------------------------------------------------------------+
//|                                  Alpha_Squeeze_Expansion_V4.mq5 |
//|                                  Copyright 2026, Quant Engineer  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link      ""
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

// 入力パラメータ
input double InpLotSize = 0.1;
input int InpTP = 600;            // Take Profit (points)
input int InpSL = 200;            // Stop Loss (points)
input int InpTrailStart = 150;    // Trailing Stop Start (points)
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

// グローバル変数
int handleBB, handleMACD, handleADX, handleEMA15, handleEMA1h;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handleBB = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
    handleMACD = iMACD(_Symbol, PERIOD_M5, 12, 26, 9, PRICE_CLOSE);
    handleADX = iADX(_Symbol, PERIOD_M5, 14);
    handleEMA15 = iMA(_Symbol, PERIOD_M15, 10, 0, MODE_EMA, PRICE_CLOSE);
    handleEMA1h = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
    
    if(handleBB == INVALID_HANDLE || handleMACD == INVALID_HANDLE || handleADX == INVALID_HANDLE)
        return(INIT_FAILED);
        
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 時間帯フィルター
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    bool timeWindow = ((dt.hour >= 16 && dt.hour < 18) || (dt.hour >= 21 && dt.hour < 24));
    if(!timeWindow) return;

    // インジケーター値の取得
    double bbu[], bbl[], macd_main[], macd_sig[], adx[], ema15[], ema1h[];
    ArraySetAsSeries(bbu, true); ArraySetAsSeries(bbl, true);
    ArraySetAsSeries(macd_main, true); ArraySetAsSeries(macd_sig, true);
    ArraySetAsSeries(adx, true); ArraySetAsSeries(ema15, true); ArraySetAsSeries(ema1h, true);

    CopyBuffer(handleBB, 1, 0, 3, bbu);
    CopyBuffer(handleBB, 2, 0, 3, bbl);
    CopyBuffer(handleMACD, 0, 0, 3, macd_main);
    CopyBuffer(handleADX, 0, 0, 3, adx);
    CopyBuffer(handleEMA15, 0, 0, 3, ema15);
    CopyBuffer(handleEMA1h, 0, 0, 3, ema1h);

    // ケルトナーチャネルの簡易計算 (SMA + 2*ATR)
    double kc_u = GetKCU(1);
    double kc_l = GetKCL(1);
    double prev_kc_u = GetKCU(2);
    double prev_kc_l = GetKCL(2);

    // スクイーズ判定
    bool isSqueezePrev = (bbu[1] < prev_kc_u && bbl[1] > prev_kc_l);
    bool isSqueezeCurr = (bbu[0] < kc_u && bbl[0] > kc_l);

    // 最高値・最安値
    double hh10 = iHigh(_Symbol, PERIOD_M5, iHighest(_Symbol, PERIOD_M5, MODE_HIGH, 10, 1));
    double ll10 = iLow(_Symbol, PERIOD_M5, iLowest(_Symbol, PERIOD_M5, MODE_LOW, 10, 1));

    double close0 = iClose(_Symbol, PERIOD_M5, 0);
    double close1 = iClose(_Symbol, PERIOD_M5, 1);

    // エントリーロジック
    if(PositionsTotal() == 0)
    {
        // BUY
        if(close0 > ema1h[0] && close0 > ema15[0]) // MTF Filter
        {
            if(isSqueezePrev && bbu[0] > kc_u) // Release
            {
                if(macd_main[0] > 0 && macd_main[0] > macd_main[1]) // Momentum
                {
                    if(close0 > bbu[0] && close0 > hh10 && adx[0] > adx[1])
                    {
                        double sl = close0 - InpSL * _Point;
                        double tp = close0 + InpTP * _Point;
                        trade.Buy(InpLotSize, _Symbol, close0, sl, tp);
                    }
                }
            }
        }
        // SELL
        else if(close0 < ema1h[0] && close0 < ema15[0]) // MTF Filter
        {
            if(isSqueezePrev && bbl[0] < kc_l) // Release
            {
                if(macd_main[0] < 0 && macd_main[0] < macd_main[1]) // Momentum
                {
                    if(close0 < bbl[0] && close0 < ll10 && adx[0] > adx[1])
                    {
                        double sl = close0 + InpSL * _Point;
                        double tp = close0 - InpTP * _Point;
                        trade.Sell(InpLotSize, _Symbol, close0, sl, tp);
                    }
                }
            }
        }
    }
    
    ManageTrailingStop();
}

double GetKCU(int shift) {
    double sma = iMA(_Symbol, PERIOD_M5, InpKCPeriod, 0, MODE_SMA, PRICE_CLOSE); // Simple for example
    double atr = iATR(_Symbol, PERIOD_M5, InpKCPeriod, shift);
    return iMA(_Symbol, PERIOD_M5, InpKCPeriod, 0, MODE_SMA, PRICE_CLOSE) + InpKCScalar * atr;
}

double GetKCL(int shift) {
    double atr = iATR(_Symbol, PERIOD_M5, InpKCPeriod, shift);
    return iMA(_Symbol, PERIOD_M5, InpKCPeriod, 0, MODE_SMA, PRICE_CLOSE) - InpKCScalar * atr;
}

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 price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
                if(price - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point) {
                    double newSL = price - InpTrailStart * _Point;
                    if(newSL > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) {
                double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
                if(PositionGetDouble(POSITION_PRICE_OPEN) - price > InpTrailStart * _Point) {
                    double newSL = price + InpTrailStart * _Point;
                    if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}