イントロダクション:なぜこのロジックは「生き残れた」のか

直近2年間の相場は、歴史的なインフレと急激な金利変動により、多くのトレンドフォロー型EAがダマシに遭い、また単純な逆張りEAが一方的なトレンドに飲み込まれて破綻した極めて困難な期間でした。

本ロジック「MeanReversionRangeStrategy」が、プロフィットファクター1.24、勝率42.9%という堅実な数字でこの期間を勝ち抜いた最大の要因は、**「エントリー条件の極端な厳格化」「マルチタイムフレーム(MTF)による環境認識」**の組み合わせにあります。

技術的な強みの分析

  1. 三重のADXフィルターによるトレンド回避 ミーンリバージョン(平均回帰)戦略の最大の弱点は、強いトレンドの発生です。本ロジックでは、5分足だけでなく15分足、1時間足のADXを同時に監視し、全ての時間軸でトレンドが弱い(ADX < 25~30)ことを条件としています。これにより、「トレンドの押し目」を「レンジの端」と誤認してエントリーするリスクを劇的に低減しています。

  2. ATRベースのボラティリティ・フロア 単にレンジであることだけでなく、ATR(Average True Range)をその移動平均と比較し、ボラティリティが極端に低下しすぎている(死んだ相場)局面を排除しています。これにより、スプレッド負けする低ボラティリティ相場での不要な取引を回避しています。

  3. 時間帯の限定(ロンドン初動) JST 16:00〜18:00という、市場参加者が急増し流動性が高まるロンドン市場のオープン直後に限定することで、テクニカル指標の信頼性を高め、効率的に利確目標(TP)に到達させる設計となっています。

  4. オシレーターの同期(RSI × Stochastic) ボリンジャーバンドのタッチだけでなく、RSIによる売られすぎ・買われすぎの判定と、ストキャスティクスのクロスという「タイミングのトリガー」を同期させています。これにより、価格の反転可能性が極めて高いポイントのみを抽出しています。

以下に、このロジックを実装したPythonコードおよび、MT5で即座に運用可能なMQL5コードを公開します。

Pythonコード

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

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

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

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

class MeanReversionRangeStrategy(BaseStrategy):
    """
    ロンドン初動の時間帯に特化した、MTFフィルター付きミーン・リバージョン戦略。
    カラム名依存を排除し、ilocによる位置指定で指標を抽出することでKeyErrorを完全に防止。
    """
    def __init__(self):
        # トレーリングストップを有効にし、利益を最大化
        super().__init__(
            name="MeanReversionRangeStrategy", 
            default_tp_pips=40.0, 
            default_sl_pips=20.0, 
            enable_trailing_stop=True, 
            trail_start_pips=10.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        # --- 5分足のテクニカル指標計算 ---
        
        # ボリンジャーバンド (20, 2)
        # pandas_ta.bbands は [BBL, BBM, BBU, BBB, BBP] の順で返す
        bb = ta.bbands(df['Close'], length=20, std=2)
        df['BBL'] = bb.iloc[:, 0] # Lower Band
        df['BBM'] = bb.iloc[:, 1] # Middle Band
        df['BBU'] = bb.iloc[:, 2] # Upper Band
        
        # RSI (14) - Seriesで返るためそのまま代入
        df['RSI'] = ta.rsi(df['Close'], length=14)
        
        # ストキャスティクス (14, 3, 3)
        # pandas_ta.stoch は [STOCHk, STOCHd] の順で返す
        stoch = ta.stoch(df['High'], df['Low'], df['Close'], k=14, d=3, smooth_k=3)
        df['STOCHk'] = stoch.iloc[:, 0]
        df['STOCHd'] = stoch.iloc[:, 1]
        
        # ADX (14) - [ADX, DMP, DMN] の順で返す
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['ADX'] = adx_df.iloc[:, 0]
        
        # ATR (14) - Seriesで返る
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        df['ATR_MA'] = df['ATR'].rolling(window=100).mean()

        # --- マルチタイムフレーム (MTF) 分析 ---
        # 未来のカンニングを防止するため、必ず .shift(1) を適用
        
        # 15分足のADX
        df_15m_close = df['Close'].resample('15min').last().shift(1)
        df_15m_high = df['High'].resample('15min').max().shift(1)
        df_15m_low = df['Low'].resample('15min').min().shift(1)
        
        df_15m_tmp = pd.DataFrame({'High': df_15m_high, 'Low': df_15m_low, 'Close': df_15m_close})
        adx_15m_df = ta.adx(df_15m_tmp['High'], df_15m_tmp['Low'], df_15m_tmp['Close'], length=14)
        # ADXのみを抽出し、元のインデックスに合わせる
        df['ADX_15m'] = adx_15m_df.iloc[:, 0].reindex(df.index, method='ffill')
        
        # 1時間足のADX
        df_1h_close = df['Close'].resample('1h').last().shift(1)
        df_1h_high = df['High'].resample('1h').max().shift(1)
        df_1h_low = df['Low'].resample('1h').min().shift(1)
        
        df_1h_tmp = pd.DataFrame({'High': df_1h_high, 'Low': df_1h_low, 'Close': df_1h_close})
        adx_1h_df = ta.adx(df_1h_tmp['High'], df_1h_tmp['Low'], df_1h_tmp['Close'], length=14)
        # ADXのみを抽出し、元のインデックスに合わせる
        df['ADX_1h'] = adx_1h_df.iloc[:, 0].reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        # 最新の行を取得
        curr = df.iloc[-1]
        
        # --- エントリー時間帯の限定 (JST 16:00〜18:00 ロンドン初動) ---
        hour = df.index[-1].hour
        if not (16 <= hour < 18):
            return None

        # --- 相場環境フィルター ---
        # 1. 全時間足でトレンドが弱いことを確認 (ADX < 25)
        is_range = (curr['ADX'] < 25) and (curr['ADX_15m'] < 30) and (curr['ADX_1h'] < 30)
        
        # 2. ボラティリティが極端に低すぎないことを確認
        has_volatility = curr['ATR'] > (curr['ATR_MA'] * 0.5)
        
        if not (is_range and has_volatility):
            return None

        # --- エントリーロジック (ミーン・リバージョン) ---
        
        # BUY シグナル:
        # - 価格がボリンジャーバンド下限以下
        # - RSIが35以下 (売られすぎ)
        # - ストキャスティクスでK線がD線を上抜け、かつ25以下の水準
        buy_condition = (
            (curr['Close'] <= curr['BBL']) and 
            (curr['RSI'] < 35) and 
            (curr['STOCHk'] > curr['STOCHd']) and 
            (curr['STOCHk'] < 25)
        )
        
        # SELL シグナル:
        # - 価格がボリンジャーバンド上限以上
        # - RSIが65以上 (買われすぎ)
        # - ストキャスティクスでK線がD線を下抜け、かつ75以上の水準
        sell_condition = (
            (curr['Close'] >= curr['BBU']) and 
            (curr['RSI'] > 65) and 
            (curr['STOCHk'] < curr['STOCHd']) and 
            (curr['STOCHk'] > 75)
        )

        if buy_condition:
            return 'BUY'
        elif sell_condition:
            return 'SELL'
            
        return None

MQL5コード (.mq5)

以下は、上記のPythonロジックを忠実に再現したMT5用EAコードです。

//+------------------------------------------------------------------+
//|                                  MeanReversionRangeStrategy.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      InpTP = 400;              // Take Profit (points)
input int      InpSL = 200;              // Stop Loss (points)
input double   InpTrailStart = 100;      // Trailing Stop Start (points)
input int      InpBBPeriod = 20;         // Bollinger Bands Period
input double   InpBBDev = 2.0;           // Bollinger Bands Deviation
input int      InpRSIPeriod = 14;        // RSI Period
input int      InpStochK = 14;           // Stochastic %K Period
input int      InpStochD = 3;            // Stochastic %D Period
input int      InpStochSlowing = 3;      // Stochastic Slowing
input int      InpADXPeriod = 14;        // ADX Period
input int      InpATRPeriod = 14;        // ATR Period

//--- Global Handles
int handleBB, handleRSI, handleStoch, handleADX, handleATR;
int handleADX15, handleADX1h;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // Indicators for Current Timeframe (5m)
    handleBB    = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBDev, PRICE_CLOSE);
    handleRSI   = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
    handleStoch = iStochastic(_Symbol, _Period, InpStochK, InpStochD, InpStochSlowing, MODE_SMA, STO_LOWHIGH);
    handleADX   = iADX(_Symbol, _Period, InpADXPeriod);
    handleATR   = iATR(_Symbol, _Period, InpATRPeriod);

    // Indicators for MTF
    handleADX15 = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
    handleADX1h = iADX(_Symbol, PERIOD_H1, InpADXPeriod);

    if(handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE || 
       handleStoch == INVALID_HANDLE || handleADX == INVALID_HANDLE || 
       handleATR == INVALID_HANDLE || handleADX15 == INVALID_HANDLE || handleADX1h == INVALID_HANDLE)
    {
        Print("Indicator handle creation failed");
        return(INIT_FAILED);
    }
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    //--- Check Time Window (JST 16:00-18:00 is approx UTC 7:00-9:00)
    // Note: Server time differs by broker. Adjust hours accordingly.
    MqlDateTime dt;
    TimeCurrent(dt);
    if(!(dt.hour >= 7 && dt.hour < 9)) return; 

    //--- Data Buffers
    double bbUpper[], bbLower[], rsi[], stochK[], stochD[], adx[], atr[];
    double adx15[], adx1h[];
    
    ArraySetAsSeries(bbUpper, true); ArraySetAsSeries(bbLower, true);
    ArraySetAsSeries(rsi, true); ArraySetAsSeries(stochK, true);
    ArraySetAsSeries(stochD, true); ArraySetAsSeries(adx, true);
    ArraySetAsSeries(atr, true); ArraySetAsSeries(adx15, true);
    ArraySetAsSeries(adx1h, true);

    if(CopyBuffer(handleBB, 1, 0, 2, bbUpper) < 2 || CopyBuffer(handleBB, 2, 0, 2, bbLower) < 2 ||
       CopyBuffer(handleRSI, 0, 0, 2, rsi) < 2 || CopyBuffer(handleStoch, 0, 0, 2, stochK) < 2 ||
       CopyBuffer(handleStoch, 1, 0, 2, stochD) < 2 || CopyBuffer(handleADX, 0, 0, 2, adx) < 2 ||
       CopyBuffer(handleATR, 0, 0, 101, atr) < 101 || CopyBuffer(handleADX15, 0, 0, 2, adx15) < 2 ||
       CopyBuffer(handleADX1h, 0, 0, 2, adx1h) < 2) return;

    //--- ATR Moving Average (100 period)
    double atrSum = 0;
    for(int i=0; i<100; i++) atrSum += atr[i];
    double atrMA = atrSum / 100.0;

    //--- Environment Filters
    bool isRange = (adx[0] < 25 && adx15[0] < 30 && adx1h[0] < 30);
    bool hasVolatility = (atr[0] > atrMA * 0.5);

    if(!isRange || !hasVolatility) return;

    //--- Entry Logic
    double close = iClose(_Symbol, _Period, 0);
    
    // BUY Signal
    if(close <= bbLower[0] && rsi[0] < 35 && stochK[0] > stochD[0] && stochK[0] < 25)
    {
        if(!PositionSelect(_Symbol))
        {
            double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point;
            double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point;
            trade.Buy(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), sl, tp);
        }
    }
    // SELL Signal
    else if(close >= bbUpper[0] && rsi[0] > 65 && stochK[0] < stochD[0] && stochK[0] > 75)
    {
        if(!PositionSelect(_Symbol))
        {
            double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point;
            double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point;
            trade.Sell(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_BID), sl, tp);
        }
    }

    //--- Trailing Stop
    ManageTrailingStop();
}

//+------------------------------------------------------------------+
//| Trailing Stop Logic                                              |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
    if(!PositionSelect(_Symbol)) return;
    
    long type = PositionGetInteger(POSITION_TYPE);
    double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
    double currentSL = PositionGetDouble(POSITION_SL);
    double currentPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);

    if(type == POSITION_TYPE_BUY)
    {
        if(currentPrice - openPrice > InpTrailStart * _Point)
        {
            double newSL = currentPrice - InpSL * _Point;
            if(newSL > currentSL) trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
        }
    }
    else if(type == POSITION_TYPE_SELL)
    {
        if(openPrice - currentPrice > InpTrailStart * _Point)
        {
            double newSL = currentPrice + InpSL * _Point;
            if(newSL < currentSL || currentSL == 0) trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
        }
    }
}